Skip to main content
ClaudeWave

Self-hosted custom domains for multi-tenant SaaS: customers bring their own hostnames, get exact DNS records and automatic HTTPS (Caddy on-demand TLS), and every request reaches your app with a signed workspace assertion. API, Python SDK, webhooks, operator portal.

HooksOfficial Registry19 stars4 forks● PythonApache-2.0Updated today
ClaudeWave Trust Score
100/100
✓ Verified
Passed
  • ✓Open-source license (Apache-2.0)
  • ✓Actively maintained (<30d)
  • ✓Clear description
  • ✓Topics declared
  • ✓Mature repo (>1y old)
  • ✓Documented (README)
Last scanned: 10/13/2026
Install as Claude Code hooks
Method: Clone
Terminal
git clone https://github.com/sireto/custom-domain
1. Clone the repository and review the hook scripts.
2. Wire them into the hooks section of ~/.claude/settings.json as the README describes.
3. Test with a harmless command before trusting them in real sessions.
Use cases

Hooks overview

# Custom Domain

Custom domains for multi-tenant SaaS products. Applications register their
customers' hostnames through an API, hand back exact DNS instructions, and are
told when each hostname is verified, certified and serving. The edge (Caddy)
obtains certificates on demand and forwards every request to the application's
origin with a signed assertion that names the workspace the hostname belongs
to, so the application never selects a tenant from the `Host` header.

Several applications share one deployment. Each has its own credentials, its
own verified origin and its own hostnames; nothing an application does can
affect another's.

Integration guides (quickstart, API, verifying requests, webhooks, and a
brief for AI coding agents) are at
[customdomainapi.com/docs](https://customdomainapi.com/docs/), also as
Markdown for agents ([llms.txt](https://customdomainapi.com/llms.txt)). The
same service is run as a hosted option at
[customdomainapi.com](https://customdomainapi.com).

## How it works

1. The operator creates an application, registers its origin and proves
   control of it (the origin serves a token at
   `/.well-known/custom-domain-origin-verification`), then issues an API
   credential.
2. The application calls `POST /v1/domains` with a customer hostname and an
   opaque workspace reference. The answer carries the two records the customer
   must publish: a TXT record proving ownership and a CNAME to the edge.
3. The lifecycle worker checks DNS, obtains the certificate through the edge
   and asks the origin, through the edge, which workspace it serves for the
   hostname. The domain becomes `ready` only when ownership, routing,
   certificate and origin checks all pass. Drift later moves it to
   `attention_required`; a lost TXT record suspends it after 24 hours.
4. Every request the edge proxies carries `X-Custom-Domain-Assertion`, an
   HMAC-signed token binding the request to the application, domain, workspace
   reference and hostname. The SDK middleware verifies it and exposes the
   workspace to the application.
5. Status changes are delivered as signed webhooks (`domain.ready`,
   `domain.attention_required`, `domain.recovered`, `domain.deleted`), with
   history and replay. Deleting a domain stops service on the next request.

Only exact customer subdomains are supported (no apex or wildcard names).

## Quick start (single container)

Create a `.env` file:

```
DATABASE_URL=sqlite:///data/custom_domain.db
ENABLE_LEGACY_API=false
EDGE_ASSERTION_KEYS=1:<at least 32 random characters>
ACME_EMAIL=ops@example.com
CADDY_STORAGE=file
```

`EDGE_ASSERTION_KEYS` signs the assertions the edge attaches to proxied
requests; generate the secret with `python -c "import secrets; print(secrets.token_urlsafe(48))"`.
Use a PostgreSQL URL such as `postgresql+psycopg://user:password@db:5432/custom_domain`
for anything beyond a trial. All settings are listed in [.env_example](.env_example).

Run the image:

```bash
docker volume create https_data && docker volume create https_db && docker volume create https_domains
docker compose up -d
```

The bundled [docker-compose.yml](docker-compose.yml) publishes ports 80 and
443 and binds the management API to `127.0.0.1:9000`. With `PUBLIC_API=true`
and `EDGE_HOSTNAME` set, the edge also serves the v1 API at
`https://<EDGE_HOSTNAME>/v1` for applications (nothing else of the API is
routed); the production installer turns this on. Swagger UI is at
**http://localhost:9000/v1/docs**; the OpenAPI document is served at
`/v1/openapi.json` and committed as [docs/openapi.json](docs/openapi.json).

For production, use [deploy/compose.production.yml](deploy/compose.production.yml):
PostgreSQL, Redis-backed certificate storage, and separate API, worker and edge
containers with the edge's admin API behind a validating gateway. The
walkthrough, backups, monitoring and rollback are in [docs/deployment.md](docs/deployment.md).

## Try it locally

[deploy/compose.local.yml](deploy/compose.local.yml) runs the whole service
on one machine with nothing else: Caddy with its own private CA, the API and
workers, and the sample SaaS origin as a second container. The DNS checks
are answered from the records the service issued (`DNS_VERIFICATION_MODE=local`,
refused unless the edge is local too), so invented hostnames under
`localtest.me` (which resolves to 127.0.0.1) go through the real lifecycle.

```bash
docker compose -f deploy/compose.local.yml up -d --build
docker compose -f deploy/compose.local.yml exec custom-domain custom-domain dev demo
```

The demo creates the sample application, registers and verifies its origin,
issues a credential (printed once) and registers `alpha.sample.localtest.me`
and `beta.sample.localtest.me` for two workspaces, then waits for them to
become `ready`. Open https://alpha.sample.localtest.me/ and
https://beta.sample.localtest.me/: each shows its own workspace, served
through Caddy with a signed assertion. The certificate is signed by the local
CA; trust it or fetch it for curl:

```bash
docker compose -f deploy/compose.local.yml cp custom-domain:/var/lib/custom-domain/local-ca.crt local-ca.crt
curl --cacert local-ca.crt https://alpha.sample.localtest.me/
```

The management API is at http://127.0.0.1:9000/v1/docs; use the printed
credential with the SDK against it. `docker compose -f deploy/compose.local.yml down -v`
removes everything.

## Hosting on a cloud server

A single small server runs everything, with fixed IPv4 and IPv6 addresses
for customers' CNAMEs to resolve to. Each guide takes about fifteen minutes,
ending with the portal open at `https://<edge hostname>/portal` and the API
at `https://<edge hostname>/v1`.

| Cloud | How | Guide |
|---|---|---|
| Azure | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsireto%2Fcustom-domain%2Fmain%2Fdeploy%2Fazure%2Fazuredeploy.json) (ARM/Bicep template) | [docs/hosting-azure.md](docs/hosting-azure.md) |
| AWS | CloudFormation stack ([deploy/aws/custom-domain.yaml](deploy/aws/custom-domain.yaml)) | [docs/hosting-aws.md](docs/hosting-aws.md) |
| Google Cloud | [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fsireto%2Fcustom-domain&cloudshell_git_branch=main&cloudshell_tutorial=deploy%2Fgcp%2Ftutorial.md&show=terminal) (guided script) | [docs/hosting-gcp.md](docs/hosting-gcp.md) |
| Hetzner Cloud | paste [deploy/cloud-init.yaml](deploy/cloud-init.yaml) as the server's cloud config | [docs/hosting-hetzner.md](docs/hosting-hetzner.md) |
| DigitalOcean | paste [deploy/cloud-init.yaml](deploy/cloud-init.yaml) as the Droplet's user data | [docs/hosting-digitalocean.md](docs/hosting-digitalocean.md) |

All of them run the same installer at a released version: Docker, generated
configuration and secrets, and the production layout, unattended.

The same script runs on any Ubuntu or Debian host as root:
`bash deploy/install.sh`, and re-running it (or `custom-domain upgrade <version>`)
is the upgrade path. Afterwards `custom-domain doctor` checks the
database and migrations, the edge gateway, the certificate authority, the
reconciler, and that each application's CNAME target reaches the edge.

## Operating it

Every operator action is available in two equivalent forms: the
`custom-domain` command, and the **portal** at `/portal` of the management
API. Both call the same code, so they never diverge.

**The portal** (set `PORTAL_PASSWORD` to enable it) is where most operators
start. It shows a setup checklist for each application, and for each
customer hostname the two DNS records with what public DNS currently returns
for them. The Edge & DNS page lists every name the edge answers for and
checks that it reaches this server. Everything below can be done there too,
including deletes, webhooks and the legacy import. Reach it at
`https://<EDGE_HOSTNAME>/portal` from the addresses in `PORTAL_ALLOWED_IPS`,
or through an SSH tunnel to port 9000 ([docs/portal.md](docs/portal.md)).

**The command** runs inside the API container (the installer puts a
`custom-domain` wrapper on the host), or with `uv run custom-domain` in a
checkout:

```bash
custom-domain application create --slug acme --name "Acme" --cname-target acme.edge.example.net
custom-domain origin register --application acme --host app.acme.example --scheme https --port 443
custom-domain origin verify --application acme --host app.acme.example --activate
custom-domain credential issue --application acme --label backend
```

`--cname-target` is the name customers point their CNAME at; it must resolve
to this deployment's edge, and defaults to `EDGE_HOSTNAME`. `origin register`
prints the verification token the application must serve before `origin
verify` succeeds. Credentials are shown once and can be rotated with a grace
period (`credential rotate`).

| Area | Commands |
|---|---|
| Applications | `application list [--deleted]`, `rename`, `set-cname-target [--reissue-claims]`, `set-workspace-probe`, `delete --confirm <slug> [--delete-domains]` |
| Origins | `origin list`, `register`, `verify`, `activate`, `retire`, `delete` |
| Credentials | `credential list`, `issue`, `rotate`, `revoke`, `delete` |
| Edge | `edge config`, `edge reconcile` (the Caddy configuration derived from the database) |
| Operations | `doctor` (checks the whole deployment), `upgrade <version>` (host wrapper), `db upgrade`, `worker run`, `domain purge-tombstones`, `legacy import` |

Deleting keeps the audit trail: a deleted hostname, or every hostname of a
deleted application, is kept as a tombstone with its history for 90 days,
then removed by `domain purge-tombstones`. `custom-domain --help` lists
every command and option.

## Integrating an application

The Python SDK in [sdk/](sdk/README.md) (`custom-domain-sdk`, released at the
same version as the service image) has 
acmecaddycnamecustom-domaincustom-domainsdnsdockerfastapihttpsletsencryptmulti-tenancymulti-tenanton-demand-tlspythonreverse-proxysaasself-hostedtlswebhookswhite-label

What people ask about custom-domain

What is sireto/custom-domain?

+

sireto/custom-domain is hooks for the Claude AI ecosystem. Self-hosted custom domains for multi-tenant SaaS: customers bring their own hostnames, get exact DNS records and automatic HTTPS (Caddy on-demand TLS), and every request reaches your app with a signed workspace assertion. API, Python SDK, webhooks, operator portal. It has 19 GitHub stars and its last recorded update is dated 2026-10-13.

How do I install custom-domain?

+

You can install custom-domain by cloning the repository (https://github.com/sireto/custom-domain) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.

Is sireto/custom-domain safe to use?

+

Our security agent has analyzed sireto/custom-domain and assigned a Trust Score of 100/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.

Who maintains sireto/custom-domain?

+

sireto/custom-domain is maintained by sireto. The last recorded GitHub activity is dated 2026-10-13, with 7 open issues.

Are there alternatives to custom-domain?

+

Yes. On ClaudeWave you can browse similar hooks at /categories/hooks, sorted by popularity or recent activity.

Deploy custom-domain to your cloud

Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.

Maintain this repo? Add a badge to your README

Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.

Featured on ClaudeWave: sireto/custom-domain
[![Featured on ClaudeWave](https://claudewave.com/api/badge/sireto-custom-domain)](https://claudewave.com/repo/sireto-custom-domain)
<a href="https://claudewave.com/repo/sireto-custom-domain"><img src="https://claudewave.com/api/badge/sireto-custom-domain" alt="Featured on ClaudeWave: sireto/custom-domain" width="320" height="64" /></a>