Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Custom Domain: the organization home

Custom Domain is the managed domain-connection platform for SaaS products. It lets your users put a domain they already own in front of your product, with the DNS records written, the ownership proven, and the TLS certificate issued and renewed for them. This repository is the front door to that organization: it holds the profile page GitHub renders at github.com/CUSTOM-DOMAIN-APP, the community health files every other repository here inherits, and the screenshots and demo capture those pages embed.

ProductDocsStatusMCPLicense

At a glance

What it isThe .github metadata repository for the Custom Domain organization: the org profile page, org-wide community health defaults, and the brand assets those pages embed.
Who it's forAnyone landing on this organization from a search result, an MCP registry entry, an npm package page, or a docs link, plus the maintainers who edit the org's public surface.
Live atcustomdomain.ai (product) · docs.customdomain.ai/docs (docs) · status.customdomain.ai (uptime)
StackMarkdown and static assets only. No build, no runtime, no dependencies. GitHub renders it directly.
StatusPublic. Active. Default branch main. Rendered live on the organization page.

About this repository

GitHub gives every organization one repository with a reserved name, .github, and reads two things out of it. The file at profile/README.md becomes the organization's front page, shown above the repository list to every visitor who opens github.com/CUSTOM-DOMAIN-APP. The community health files at the root become the defaults for every repository in the organization that does not ship its own copy, so a contributor who clicks "Report a vulnerability" in any of the fifteen repositories here reaches the same policy.

That makes this repository small but load-bearing. It is the only place in the organization where a stranger's first impression is written, and the only place a security report gets routed from. It carries no code, no build step and no dependencies: four Markdown files, one profile page, and four committed image assets that the profile page embeds by relative path.

The rest of this document is split in half on purpose. Everything above the Quickstart explains what Custom Domain is and who it is for, because that is what a first-time visitor needs. Everything below it explains how this repository and the organization's public surface are put together, because that is what a maintainer needs.

The problem

Every multi-tenant product eventually hits the same request: a customer wants the product to live at app.acme.com instead of acme.yourproduct.com. It sounds like a small feature. It is not. There are dozens of DNS providers with incompatible record models and no shared API. Apex domains cannot hold a CNAME at all under RFC 1034, so half your customers need a different record type than the other half. Certificates have to be issued and then renewed forever, per customer, without anyone thinking about it. And every one of those steps happens in an account you do not control, by a person who has never opened a DNS panel.

So the feature ships as a support-ticket generator. The customer is handed a table of records to copy, gets one of them wrong or puts it on the wrong host, and the connection sits in "pending" while nobody can tell them why. Teams that build this in-house typically spend weeks on it and then keep paying for it in a queue of "my domain says pending" tickets that never fully goes away. The work is unglamorous, it is nobody's differentiator, and it is genuinely hard to get right for the long tail of registrars.

What it does

Custom Domain replaces that whole surface with one connect flow that you embed, call, or hand to an agent. Your user types their domain. The platform works out where its DNS actually lives, picks the best rail it can use for that provider, writes the records, watches public DNS until they resolve to the intended values, and issues a certificate at a managed edge that then terminates TLS for that hostname.

  • Connects a domain in about thirty seconds on the one-click authorization path, from the moment the user types it to the moment the hostname serves HTTPS.
  • Covers 63 DNS and registrar providers through a single census, with 25 or more of them configured fully automatically and no dead end anywhere else: providers that offer scoped API tokens take a pasted token, and everything else falls back to a guided manual flow whose records are verified automatically once they appear.
  • Issues and renews real Let's Encrypt certificates at the edge. Verified end to end in July 2026 against a live connected domain: a browser-trusted production certificate, valid chain, issued on first handshake and cached thereafter.
  • Proves control without a verification chore. There is no separate ownership challenge to paste. Control is established by the rail that writes the records (an authorization at the provider, a one-click apply, or a scoped token), or by the records showing up in the domain's own authoritative DNS, checked by value rather than by presence.
  • Handles email domains through the same flow, writing MX, SPF, DKIM and DMARC records from server-side templates rather than asking anyone to hand-assemble an SPF string.
  • Gives AI agents a first-class path. The hosted MCP server exposes twelve tools over streamable HTTP, speaks protocol revision 2025-06-18, and is listed in the official MCP registry as ai.customdomain/mcp. No tool accepts a raw DNS record as input, which closes off the prompt-injection paths that would otherwise end in arbitrary DNS writes.
  • Isolates tenants strictly. A July 2026 black-box production audit walked the API as a second tenant and got 404 on every resource belonging to the first, with no cross-tenant leakage found.

Two of the platform's Domain Connect templates are merged upstream into the Domain-Connect/Templates registry (#1323 and #1340), which is what lets a supporting provider apply the record set from its own dashboard rather than from ours.

Pricing starts at $0: the free Starter plan includes ten domain connections per year and the full product surface, widget, API, automatic TLS, monitoring and the MCP server included.

Who it is for

If your product has tenants, and your tenants have brands, this is for you.

AudienceThe shape of the needStart here
Website buildersEvery published site wants its own name in the address bar, at tenant scale.connect-domain-for-website-builders
Email platformsSending domains need SPF, DKIM, DMARC and return-path written correctly the first time.connect-domain-for-email-platforms
AI agentsAn agent that ships a site needs a real domain: search it, buy it, connect it, without a human in the loop.connect-domain-for-ai-agents
AgenciesClient domains connected under your own brand, without collecting anyone's registrar login.connect-domain-for-agencies

It also fits e-commerce platforms giving each merchant a storefront domain, creator tools giving each creator a domain, and any multi-tenant SaaS where a shared subdomain is the thing customers complain about. The common thread is not the industry, it is the shape: you have many customers, each customer has a brand, and the address bar is the most visible place that brand either shows up or does not. Products that get this right stop looking like a tool their customers rent and start looking like something their customers own, which is why custom domains keep appearing near the top of enterprise-plan feature lists rather than in the backlog. The cost of getting it wrong is equally consistent: a queue of tickets that says "pending" and a sales objection nobody on the team can answer. The vocabulary, if the category is new to you, is in the glossary: bring your own domain and custom domain vs subdomain.


Quickstart

There is nothing to install to work on this repository. Clone it, edit Markdown, push.

git clone https://github.com/CUSTOM-DOMAIN-APP/.github.git
cd .github
# The org front page. This is the file GitHub renders at github.com/CUSTOM-DOMAIN-APP.$EDITOR profile/README.md
# Check every link still resolves before you push (see Testing, below).
grep -oE 'https?://[^)"< ]+' profile/README.md README.md | sort -u | \
whileread -r u;doprintf'%s %s\n'"$(curl -sL -o /dev/null -w '%{http_code}' --max-time 12 "$u")""$u";done
git commit -am "profile: <what changed>"&& git push

To use the product instead, connect a domain from the command line with an API key from the console. Keys look like sk_live_... and are shown once, at creation.

export CUSTOMDOMAIN_API_KEY="$(op read op://EJ-Products/CustomDomain-api-key-prod/api_key)"# 1. Create a connection for your user's domain.
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "app.customer.com", "application_id": "<application_id>"}'# 2. Poll until it reports connected: records written, DNS observed, TLS issued.
curl https://api.customdomain.ai/v1/connections/<connection_id> \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Or point an MCP client at the hosted server and let an agent do it. Nothing to install and nothing to keep running:

claude mcp add --transport http customdomain https://mcp.customdomain.ai/mcp \
--header "Authorization: Bearer $CUSTOMDOMAIN_API_KEY"

Exact request and response shapes live in the API reference; the tool catalog lives in customdomain-mcp.

Repository layout

Nine tracked files. Every one of them is either rendered by GitHub or embedded by something that is.

.github/
├── README.md You are here. Explains the org and this repository.
├── profile/
│ └── README.md THE ORG FRONT PAGE. GitHub renders this at github.com/CUSTOM-DOMAIN-APP.
├── CONTRIBUTING.md Org-wide contribution defaults: where docs live, house style.
├── SECURITY.md Org-wide vulnerability policy and the reporting address.
├── SUPPORT.md Org-wide support routing: docs, discussions, book a call.
└── assets/
├── connect-flow-demo.gif 3.5 MB. A real domain going live, start to finish.
├── provider-logos.png 769 KB. The supported provider wall.
├── app-connect-domain.jpg 39 KB. The console, mid-connect.
└── app-domain-live.jpg 40 KB. The same domain, live on HTTPS.

Two constraints are worth knowing before you edit. First, profile/README.md references assets with a relative ../assets/... path, which resolves correctly on the organization page because GitHub serves the profile from this repository's tree; a rename of assets/ breaks the front page silently. Second, GitHub's Markdown sanitizer strips inline <svg> markup entirely, so every image here is a committed file referenced by <img src>, never inline vector markup. That is a hard constraint, not a stylistic choice.

Architecture of the public surface

This repository is one of four layers that make up everything a stranger can see. Nothing here runs; the layer boundaries are about who renders what.

LayerRendered bySource of truthWhat it is for
Organization profileGitHub, from profile/README.mdThis repositoryThe 30-second pitch and the map of every repository
Community health defaultsGitHub, from the root *.md filesThis repositoryOne security policy, one contributing guide, one support route, inherited org-wide
Documentation siteFumadocs and Next.js in the docs repositoryCUSTOM-DOMAIN-APP/docs100+ MDX pages: guides, concepts, and 60+ API reference pages
Product surfacesThe private product monorepoCUSTOM-DOMAIN-APP/custom-domainsThe console, REST API, edge, widget and MCP server

The important property is that no layer copies another. The docs site reads its own content/ directory directly, with no sync step in the path a reader sees. This README and the profile README are separately authored and neither is generated. When they disagree, the repository that owns the subject wins: the docs repository owns behavior, this one owns positioning.

Repository map and package boundaries

Fifteen repositories, one product. Public unless noted.

RepositoryLanguageWhat it holds
docsMDXThe documentation source of truth, plus the Fumadocs renderer in site/ that serves docs.customdomain.ai
customdomain-sdkTypeScriptThe browser SDK (customdomain-js), the React wrapper (@customdomain/react), and the widget bundle they load
customdomain-mcpMarkdownThe hosted MCP server's public face: twelve tools, auth model, client configs for Claude, Cursor and ChatGPT
custom-domain-checksJavaScriptA GitHub App that posts a domain-health check on every push: DNS resolution, target correctness, CAA, certificate expiry, HTTPS enforcement
awesome-custom-domainsMarkdownThe curated map of the category: managed services, DIY building blocks, protocols, examples
connect-domain-for-website-buildersMarkdownUse-case guide: records, verification, TLS at tenant scale, connect-flow UX
connect-domain-for-email-platformsMarkdownUse-case guide: sending-domain onboarding, SPF, DKIM, DMARC, return-path, deliverability
connect-domain-for-ai-agentsMarkdownUse-case guide: the MCP server, the API flow, agent-safe DNS security
connect-domain-for-agenciesMarkdownUse-case guide: fleet-scale client domains, white-label connection, drift monitoring
customdomain-brand-kitAssetsLogos, wordmarks, color tokens, and the build script that derives every other asset from them
.githubMarkdownThis repository
custom-domainsGo, TypeScriptPrivate. The product monorepo: Go control plane and TLS-terminating edge, Next.js console, widget, MCP service
customdomain-siteTypeScriptPrivate. The marketing site at customdomain.ai (Next.js, HeroUI, Tailwind)
dashboard-custom-domainTypeScriptPrivate. The internal marketing engine behind marketing.customdomain.ai
CD-APPENDIXMarkdownPrivate. Product profile, architecture notes and diligence material

The boundary that matters most: the SDK never talks to a DNS provider. It opens the widget, the widget talks to the control plane, and only the control plane holds provider credentials and writes records. That is what makes it safe to embed in a customer's settings page, and it is why the widget token is minted server-side rather than shipping an API key to a browser.

How it works: a domain from typed to live

 your app ──► widget / REST / MCP ──► control plane
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
provider detect write records poll public DNS
(NS + zone walk) (OAuth · token · Domain (by value, ~60s)
Connect · guided manual)
│
▼
edge issues Let's Encrypt cert
(TLS-ALPN-01 :443 · HTTP-01 :80)
│
▼
hostname serves HTTPS ──► status: live
  1. Detect. The control plane resolves the domain's nameservers, walks up the zone for subdomains, and works out which provider actually answers for it and which rails that provider supports.
  2. Write. The best available rail applies the record set. In descending order of user effort that is: a one-click authorization at the provider, a Domain Connect apply, a scoped API token the user pastes, or a guided manual list the user copies.
  3. Observe. A background poller re-checks pending and propagating connections about once a minute and compares each record against public DNS by value, not merely by presence. Polling faster than that gains nothing.
  4. Issue. Once the connection reaches live or propagating, the edge asks the control plane whether the hostname is approved, then issues a Let's Encrypt certificate on the first TLS handshake and caches it.
  5. Serve. The edge terminates TLS for the customer hostname and proxies to your origin.

Connections move pending to propagating to live, with failed as the one terminal error. Key completion off the boolean connected rather than the status string, so your code survives the enum growing. Automatic rails give up after 24 hours in propagating; manual connections get 72 hours from pending, because a human has to get to their DNS panel. Both failure states clear on their own if the connection later verifies, so failed is recoverable and is never a reason to tell a user to start over.

Configuration

This repository has no runtime and therefore no environment variables. Its configuration is the set of paths GitHub reads by convention, plus repository metadata set through the API.

ConfigurationWhere it livesEffect
Organization front pageprofile/README.mdRendered above the repository list on the org page
Contribution defaultsCONTRIBUTING.mdInherited by every repo in the org without its own copy
Security policySECURITY.mdPowers "Report a vulnerability" org-wide; routes to legal@customdomain.ai
Support routingSUPPORT.mdShown in the new-issue chooser org-wide
Repository description, homepage, topicsGitHub repository metadatagh repo edit, not a file in the tree

Integrators consuming the platform supply exactly three credential shapes. All three are secrets. None of them belongs in a file that is committed anywhere, in this organization or in yours. Reference them from a secret manager and inject at run time.

NameWhat it isScopeReference
CUSTOMDOMAIN_API_KEYConsole API key, sk_live_... or sk_test_..., shown once at creation and stored only as a hashThe whole tenant: every application, connection, webhook and billing recordop://EJ-Products/CustomDomain-api-key-prod/api_key
APPLICATION_IDThe application identifier, also called CLIENT_ID in OAuth contexts. Not a secret on its ownOne applicationop://EJ-Products/CustomDomain-app-prod/application_id
CLIENT_SECRETReturned once when an application is created. Its only job is minting short-lived JWTsOne applicationop://EJ-Products/CustomDomain-app-prod/client_secret

Prefer the JWT path over a raw API key whenever an agent or a service only needs one application: exchange APPLICATION_ID and CLIENT_SECRET at POST /token for an hour-long bearer token. A leaked sk_live_ key carries the entire tenant; a leaked JWT expires. Widget tokens follow the same rule and are minted server-side, never in the browser.

Deployment and operations

This repository ships by being pushed. There is no workflow, no build and no artifact.

git push origin main
# GitHub re-renders github.com/CUSTOM-DOMAIN-APP within seconds.# Assets are served from raw.githubusercontent.com/CUSTOM-DOMAIN-APP/.github/main/assets/...

Three operational notes for anyone editing it:

  • Community health inheritance is opt-out, not opt-in. Adding a file here changes the behavior of fifteen repositories at once. A repository that ships its own SECURITY.md keeps it; every other one starts pointing at this one on the next page load.
  • Asset weight is a real cost.connect-flow-demo.gif is 3.5 MB and loads on every visit to the organization page. Anything added to assets/ should earn its bytes, and new screenshots should stay in the tens of kilobytes, like the two already there.
  • Renaming this repository breaks the org profile. GitHub only reads profile/README.md from a repository literally named .github.

Where the rest of the platform runs, for context: the private product monorepo deploys the Go control plane and edge together on AWS, with the edge holding its own elastic IP so it can own ports 80 and 443 for customer hostnames; the docs container rebuilds from the docs repository's content/; and the SDK publishes to npm from a tagged release workflow using npm provenance and Trusted Publishing, so no long-lived npm token sits in the repository. Live availability for all of it is published at status.customdomain.ai, and the control plane reports its own build on an unauthenticated endpoint, which is the fastest way to confirm what is actually deployed:

curl -s https://api.customdomain.ai/v1/config
# {"api_version":"v1","env":"production","service":"customdomain-control-plane",# "products":["connect","secure","sell","power","monitor","mcp"],"version":"0.1.400"}

Testing

There is no test suite to run here, and inventing one would be theater. What this repository is actually verified against is link rot and render correctness, both of which are cheap to check and are the only two ways it can be wrong.

# 1. Every link resolves. Use GET, not HEAD: several hosts answer 405 to HEAD.
grep -ohE 'https?://[^)"< ]+'*.md profile/*.md | sed 's/[.,)]*$//'| sort -u | \
whileread -r u;do
code=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 15 "$u")
[ "$code"= 200 ] ||printf'FAIL %s %s\n'"$code""$u"done# 2. Every relative asset reference points at a file that exists.
grep -ohE '\.\./assets/[A-Za-z0-9._-]+' profile/README.md | sort -u | \
whileread -r a;do [ -f"${a#../}" ] ||echo"MISSING $a";done# 3. No secret ever lands in the tree.
grep -rnE 'sk_live_|sk_test_|ghp_|AKIA[0-9A-Z]{16}'. --exclude-dir=.git &&echo"STOP"||echo"clean"

Run the first check before every push. A dead link on the organization front page is the most visible defect this repository can have, and it is the one that decays without anyone touching the file.

Contributing

Corrections are welcome, including typo-only pull requests. Open one against main.

  • Product documentation lives in CUSTOM-DOMAIN-APP/docs under content/, not here. Merged changes reach the live docs site directly.
  • Use-case guides live in the four connect-domain-for-* repositories and welcome provider notes, corrections and clearer examples.
  • The tools list in awesome-custom-domains accepts anything genuinely relevant to connecting customer-owned domains: one line per tool, alphabetical within its section, factual descriptions.

House style, applied everywhere in this organization: plain language, short paragraphs, tables where they clarify, no em dashes or en dashes, American English, and real DNS and TLS facts only. No invented numbers. If you cannot source a figure, leave it out.

Questions and ideas belong in Discussions on the docs repository.

Security

Report vulnerabilities by email to legal@customdomain.ai with reproduction steps; reports are acknowledged within two business days. In scope: the customdomain.ai product and APIs, the hosted MCP server, the embeddable widget, and every repository in this organization. Please do not open a public issue for a security report. Security posture, compliance frameworks and sub-processors are published at trust.customdomain.ai. Full policy: SECURITY.md.

License

This repository holds documentation and brand assets and carries no separate license file; the assets in assets/ are Custom Domain brand material and are not licensed for reuse. The organization's code is open: custom-domains and customdomain-sdk are Apache-2.0, and customdomain-mcp, docs, custom-domain-checks, awesome-custom-domains and the four connect-domain-for-* guides are MIT. Each repository carries its own LICENSE file and that file governs.


Custom Domain is built and operated by EVERJUST.

Connect your first domain free · Read the docs · Book a call

About

Custom Domain by EVERJUST — customdomain.ai. Let your SaaS users connect their own domain in about 30 seconds: provider detection, automatic DNS, CNAME/TXT ownership verification and Let's Encrypt TLS. REST API, embeddable widget, JS/TS SDK and a hosted MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors