# Custom domains

> Attach a customer-owned domain to Apoxy's default Gateway with zero-downtime TLS provisioning.

A `DomainRecord` binds a hostname you own (e.g., `api.example.com`) to a Gateway, Proxy,
Tunnel, or EdgeFunction on Apoxy. Apoxy handles DNS delegation, ACME certificate issuance, and
renewal.

## How it works

For customer-owned hostnames, Apoxy uses a CNAME model. You keep authority over your zone;
Apoxy only needs you to point the hostname at a zone we control (`cname.apoxy.net`) so we can
serve traffic and issue certificates on your behalf.

## Minimal setup

```yaml title="domain.yaml"
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
metadata:
  name: api.example.com
spec:
  name: api.example.com
  tls: {}
  target:
    ref:
      group: gateway.apoxy.dev
      kind: Gateway
      name: default
```

Apply the DomainRecord, then create the CNAME at your registrar:

```
api.example.com.   CNAME   api.example.com.cname.apoxy.net.
```

Apoxy finds your project-scoped target via `apoxy domainrecord get <name> -o yaml` — the
controller puts the exact value into `status`.

Once the CNAME propagates, Apoxy issues a TLS certificate using the ACME **HTTP-01** challenge
(Let's Encrypt / Google Trust Services). This takes roughly 30–60 seconds.

## Zero-downtime migration (pre-issuance)

The default HTTP-01 flow has a side effect: the certificate is only issued **after** you flip
your main CNAME. Until `status.conditions.TLSReady=True`, browsers hitting the new endpoint see
a certificate error — typically a 30–60 second window.

To eliminate that window, delegate the ACME validation to Apoxy **before** you cut over:

### Step 1 — apply the DomainRecord

Same as above.

### Step 2 — add the ACME challenge delegation CNAME

At your registrar, create an additional CNAME:

```
_acme-challenge.api.example.com.   CNAME   _acme-challenge.api.example.com.cname.apoxy.net.
```

**The target must be `_acme-challenge.<your-full-domain>.cname.apoxy.net.`** — include every
label of your hostname verbatim, including the TLD. Dropping the TLD or a mid-label is the
most common mistake and the delegation check will never pass.

<DocsTable>
| Hostname             | Correct target                                                | Common wrong target                                                            |
|----------------------|----------------------------------------------------------------|--------------------------------------------------------------------------------|
| `api.example.com`    | `_acme-challenge.api.example.com.cname.apoxy.net.`            | `_acme-challenge.api.example.cname.apoxy.net.` (missing `.com`)                |
| `app.example.co.uk`  | `_acme-challenge.app.example.co.uk.cname.apoxy.net.`          | `_acme-challenge.app.example.co.cname.apoxy.net.` (missing `.uk`)              |
</DocsTable>

If you want the exact string without typing it yourself, `apoxy domainrecord get <name> -o
yaml` prints the expected target in the `ACMEChallengeDelegationReady` condition message.

This tells the ACME CA to look up the validation TXT record inside our zone, so we can prove
domain ownership via **DNS-01** without any traffic flowing through Apoxy yet.

### Step 3 — wait for the certificate

Apoxy detects the delegation and pre-issues the certificate. Watch the conditions:

```bash title="terminal"
apoxy domainrecord get api.example.com -o yaml
```

You should see:

```yaml
status:
  conditions:
  - type: ACMEChallengeDelegationReady
    status: "True"
    reason: ACMEChallengeDelegationConfigured
  - type: TLSReady
    status: "True"
    reason: CertificateIssued
  - type: CNAMEConfigured
    status: "False"      # main record not flipped yet — expected
    reason: CNAMENotConfigured
```

`TLSReady=True` will land before `CNAMEConfigured=True`. At that point the certificate is
sitting in our gateway, ready for traffic.

### Step 4 — flip the main CNAME

Now change your main record at your registrar:

```
api.example.com.   CNAME   api.example.com.cname.apoxy.net.
```

The next DNS resolution hits Apoxy. Our gateway serves the already-issued certificate from the
first request — no cert error, no refresh loop.

### Step 5 — keep the delegation CNAME

Leave `_acme-challenge.api.example.com` pointing at our zone. Renewal uses the same DNS-01
path, so keeping the delegation means renewals stay zero-intervention.

## Zero-downtime migration — HTTP-only (no TLS)

If your `DomainRecord` leaves `spec.tls` unset (HTTP-only), there is no ACME certificate whose
issuance could double as proof of ownership. Apoxy instead accepts an explicit ownership
challenge at a dedicated label:

```
_apoxy-challenge.api.example.com.   CNAME   _apoxy-verify.cname.apoxy.net.
```

The target is a **single fixed string** — no FQDN embedding, no per-domain token. Copy it
verbatim.

Once the challenge resolves, the controller marks the `DomainRecord` with
`OwnershipVerified=True` and admits the hostname to the data plane. From that point, the
moment you flip your main CNAME the gateway starts serving traffic for it — the same
zero-downtime effect as the TLS flow, without needing a certificate.

TLS customers get `OwnershipVerified=True` for free once their cert lands (a live cert from
ACME *is* ownership proof). Setting `_apoxy-challenge` in addition is optional for TLS — the
`_acme-challenge` delegation already covers both the pre-issued cert and ownership.

You can leave the `_apoxy-challenge` delegation in place permanently; it's idempotent and
doesn't need to be touched across renewals or cutovers.

## Condition reference

<DocsTable>
| Condition                       | Meaning                                                                                                                                                                                                                                                              |
|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `OwnershipVerified`             | **Gates serving.** True when Apoxy has proof you control this hostname — either via `TLSReady=True` (ACME verified), a valid `_apoxy-challenge` CNAME, or (for non-TLS records) a successful main-CNAME cutover. This is what the edge gateway actually checks before admitting the hostname. |
| `TLSReady`                      | A certificate has been issued and is ready to serve.                                                                                                                                                                                                                  |
| `ACMEChallengeDelegationReady`  | Your `_acme-challenge.<fqdn>` CNAME points into our managed zone, so we can pre-issue certs via DNS-01.                                                                                                                                                              |
| `CNAMEConfigured`               | Your main CNAME currently resolves to our zone. Operator-facing; does **not** gate serving.                                                                                                                                                                          |
| `TargetReady`                   | The backend (Gateway / Proxy / Tunnel / EdgeFunction) this record points at is healthy.                                                                                                                                                                              |
| `Ready`                         | Aggregate: all of the customer-visible setup above is green. Dashboards and CLI show this; the data plane does not key on it.                                                                                                                                        |
</DocsTable>

## Fallback

If you skip step 2 (no `_acme-challenge` CNAME), Apoxy falls back to HTTP-01 after you flip
the main CNAME. That still works — we will try to create certificates on the fly and you just
get the standard 30–60s window between cutover and `TLSReady=True`.

## Troubleshooting

- **`ACMEChallengeDelegationReady=False` after adding the CNAME** — DNS caching. Wait up to
  the record's TTL at the customer resolver, then re-check with `dig CNAME
  _acme-challenge.api.example.com`.
- **`TLSReady=False` with reason `CertificateError`** — check the status message. Common
  causes: delegation CNAME points to the wrong target (verify it ends in
  `.<cname-zone-suffix>.` exactly — see the label-match table in Step 2), or the ACME account
  is rate-limited.
- **Want to see what target to use** — `apoxy domainrecord get <name> -o yaml` shows the full
  delegation target in the condition message.
- **Fixed a typo in the delegation CNAME but it still shows `False`** — two DNS caches may
  hold stale answers until they expire:
  - The **old CNAME value** at recursive resolvers — pinned for the TTL you set at your
    registrar (commonly 300s–3600s).
  - The **NXDOMAIN for the broken target** inside `cname.apoxy.net` — cached up to the zone's
    SOA MINIMUM TTL, **300s (5 minutes)**.

  To minimize this window, drop your registrar's CNAME TTL (e.g. to 60s) **before** you first
  publish it, so any typo is easy to correct. If you've already been bitten, there's no
  shortcut — wait out the longer of the two TTLs, then `apoxy domainrecord get <name> -o yaml`
  should flip `ACMEChallengeDelegationReady=True` on the next reconcile (runs every ~30s).
- **ACME rate-limit errors** — issuance workflows honor the CA's `Retry-After` header and
  sleep in-place instead of thrashing, so a single rate-limited domain will not burn the
  shared quota. A certificate that was in flight during a rate-limit event will resume
  automatically once the window clears; check `apoxy domainrecord get <name>` — `TLSReady`
  will flip to `True` without further action.
