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.
- ✓Open-source license (Apache-2.0)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Mature repo (>1y old)
- ✓Documented (README)
git clone https://github.com/sireto/custom-domainResumen de Hooks
# 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 | [](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 | [](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
Lo que la gente pregunta sobre custom-domain
¿Qué es sireto/custom-domain?
+
sireto/custom-domain es hooks para el ecosistema de Claude AI. 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. Tiene 19 estrellas en GitHub y su última actualización registrada es del 2026-10-13.
¿Cómo se instala custom-domain?
+
Puedes instalar custom-domain clonando el repositorio (https://github.com/sireto/custom-domain) o siguiendo las instrucciones del README en GitHub. ClaudeWave también te ofrece bloques de instalación rápida en esta misma página.
¿Es seguro usar sireto/custom-domain?
+
Nuestro agente de seguridad ha analizado sireto/custom-domain y le ha asignado un Trust Score de 100/100 (tier: Verified). Revisa el desglose completo de comprobaciones superadas y flags en esta página.
¿Quién mantiene sireto/custom-domain?
+
sireto/custom-domain es mantenido por sireto. La última actividad registrada en GitHub es del 2026-10-13, con 7 issues abiertos.
¿Hay alternativas a custom-domain?
+
Sí. En ClaudeWave puedes explorar hooks similares en /categories/hooks, ordenados por popularidad o actividad reciente.
Despliega custom-domain en tu cloud
Lleva este repo a producción en minutos. Cada plataforma genera su propio entorno con variables de entorno editables.
¿Mantienes este repo? Añade un badge a tu README
Pega el badge en tu README de GitHub para mostrar que está auditado por ClaudeWave. Cada badge enlaza de vuelta a esta página y muestra el Trust Score actual.
[](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>Más Hooks
Allows you to emulate an Android native library, and an experimental iOS emulation
Master Claude Code Hooks
Real-time monitoring for Claude Code agents through simple hook event tracking.
115+ production-ready React Hooks for sensors, UI, state & browser APIs. Tree-shakable, SSR-safe, TypeScript-first. Used by Shopee, PDD & Ctrip. Inspired by VueUse.
Make your coding agent abide by all your project rules
A webhook service that connects Claude Code to GitHub repositories, enabling AI-powered code assistance directly through pull requests and issues. This integration allows Claude to analyze repositories, answer technical questions, and help developers understand and improve their codebase through simple @mentions.