> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.unkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Domain Connect

> How our one-click custom domain DNS setup works, and how it's wired together.

## What is Domain Connect?

[Domain Connect](https://www.domainconnect.org/) is an open protocol that lets service providers (us) configure DNS records on a user's domain with one click, instead of asking them to copy-paste CNAME and TXT values manually. The user gets redirected to their DNS provider (e.g. Cloudflare), approves the changes, and the records are created automatically.

## How it works in our stack

```
User adds custom domain in dashboard
        ↓
ctrl API: AddCustomDomain
  1. Generates CNAME target + verification token
  2. Looks up domain's nameservers
  3. Checks if the DNS provider supports Domain Connect
     (via _domainconnect.{provider} TXT lookup)
  4. If yes: builds a signed redirect URL using our private key
  5. Stores domain + DC provider/URL in custom_domains table
        ↓
Dashboard shows "Automatic setup available" card
  → User clicks "Connect"
  → Redirected to DNS provider's consent page
  → Provider creates CNAME + TXT records
  → Redirects back to app.unkey.com/{workspace}/projects/{project}/settings
        ↓
Existing verification worker picks up the records (polls every 1 min)
  → ACME certificate issued
  → Frontline route created
  → Domain is live
```

## Components

### Template

The Domain Connect template defines what DNS records we need. It lives in the public [Domain-Connect/templates](https://github.com/Domain-Connect/templates) repo as [`unkey.com.custom-domain.json`](https://github.com/Domain-Connect/templates/blob/master/unkey.com.custom-domain.json).

| Setting              | Value                     | Why                                       |
| -------------------- | ------------------------- | ----------------------------------------- |
| `providerId`         | `unkey.com`               | Our provider identifier                   |
| `serviceId`          | `custom-domain`           | Service identifier                        |
| `hostRequired`       | `true`                    | Subdomains only (apex uses manual setup)  |
| `syncBlock`          | `false`                   | Synchronous flow (required by Cloudflare) |
| `syncPubKeyDomain`   | `domainconnect.unkey.com` | Where providers fetch our public key      |
| `syncRedirectDomain` | `app.unkey.com`           | Where providers redirect after approval   |

Records created:

| Type  | Host     | Value                                                       |
| ----- | -------- | ----------------------------------------------------------- |
| CNAME | `@`      | `%target%` (full CNAME target, e.g. `abc123.unkey-dns.com`) |
| TXT   | `_unkey` | `unkey-domain-verify=%verificationToken%`                   |

To update the template, open a PR against [Domain-Connect/templates](https://github.com/Domain-Connect/templates). Use the [dc-template-linter](https://github.com/Domain-Connect/dc-template-linter) to validate: `dc-template-linter -cloudflare unkey.com.custom-domain.json`.

### Signing keypair

Domain Connect requires all requests to be digitally signed (RS256). We have an RSA keypair:

* **Public key**: published as DNS TXT records at `_dcpubkeyv1.domainconnect.unkey.com`, split into two parts (`p=1` and `p=2`) due to TXT record size limits
* **Private key**: stored in AWS Secrets Manager under `unkey/control` as `UNKEY_DOMAIN_CONNECT_PRIVATE_KEY` (PEM format)

The DNS records are managed via Pulumi in [`infra/pulumi/projects/dns/unkey-com/main.go`](https://github.com/unkeyed/infra/blob/main/pulumi/projects/dns/unkey-com/main.go).

### Discovery library

We use [`railwayapp/domainconnect-go`](https://pkg.go.dev/github.com/railwayapp/domainconnect-go) which handles:

* DNS provider discovery (NS lookup → `_domainconnect.{provider}` TXT check)
* Sync URL construction with all required parameters
* RS256 signing with our private key

The wrapper is in `pkg/dns/domainconnect/discover.go`.

### Code

Discovery and signing live in `pkg/dns/domainconnect/`. The ctrl service calls `Discover()` during `AddCustomDomain` and persists the result. If no private key is configured, Domain Connect is silently disabled.

## Supported DNS providers

Any provider that publishes a `_domainconnect.{provider-domain}` TXT record is automatically supported. As of now:

| Provider                                             | Notes                                                                                                 |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Cloudflare                                           | Template onboarded via email to [domain-connect@cloudflare.com](mailto:domain-connect@cloudflare.com) |
| Vercel DNS                                           | Auto-discovered                                                                                       |
| DigitalOcean, Name.com, Hostinger, Dynadot, Namesilo | Use Cloudflare under the hood                                                                         |
| IONOS                                                | Own Domain Connect endpoint                                                                           |

To onboard with a new provider, they need to pull our template from the templates repo. Some providers (like Cloudflare and Vercel) require manual registration via email.

## Key rotation

If you need to rotate the signing keypair:

1. Generate a new keypair:

```bash theme={"theme":"kanagawa-wave"}
openssl genrsa -out domain-connect-private.pem 2048
openssl rsa -in domain-connect-private.pem -pubout -outform DER \
  | base64 | tr -d '\n' > domain-connect-pubkey.b64
```

2. Update the DNS TXT records at `_dcpubkeyv1.domainconnect.unkey.com` with the new public key chunks

3. Update `UNKEY_DOMAIN_CONNECT_PRIVATE_KEY` in AWS Secrets Manager (`unkey/control`)

4. Restart ctrl service to pick up the new key

Existing signed URLs in the database will become invalid. Users will need to re-add their domain to get a new signed URL.

## Verifying the setup

### Check public key is published

```bash theme={"theme":"kanagawa-wave"}
dig TXT _dcpubkeyv1.domainconnect.unkey.com
```

### Verify a signature

Go to [exampleservice.domainconnect.org/sig](https://exampleservice.domainconnect.org/sig), enter:

* **Key**: `_dcpubkeyv1`
* **Domain**: `domainconnect.unkey.com`
* Paste the query string and signature from a generated URL

### Validate template

```bash theme={"theme":"kanagawa-wave"}
go install github.com/Domain-Connect/dc-template-linter@latest
dc-template-linter -cloudflare unkey.com.custom-domain.json
```
