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

# Custom Domain

> Configure a custom subdomain for your Frost Auth API base URL.

# Custom Domain

Custom domains let you serve the Frost Auth API from your own subdomain instead of the default host. This is useful when ISPs block requests to shared API infrastructure, or when you want your clients hitting a domain you control.

> [!WARNING]
>
> Custom domains are for the **API only**. The customer panel (`/panel/:owner/:product`) always runs on the main FrostAuth host and cannot be mapped to a custom domain.

> [!NOTE]
>
> Custom domains require the **Pro** plan. Starter and Developer plans do not include this feature.

## How it works

You point a CNAME record at `cname.frostauth.cc`, save the hostname in your product settings, then verify it. Once verified, the domain is added to the CORS allow-list and the edge TLS terminator begins issuing a certificate for it. Your SDKs and client apps then use `https://your-domain/api/v1` as their base URL instead of the default.

```text theme={null}
api.yourdomain.com  →  CNAME  →  cname.frostauth.cc
```

***

## Setup

### Step 1 — Navigate to Settings

Open the console, select your product, and go to **Settings → Custom Domain**.

### Step 2 — Enter your subdomain

Type the hostname you want to use (e.g. `api.yourdomain.com`).

* Use a bare hostname — no `https://`, no port, no path.
* The input is lowercased and trimmed automatically.
* The domain must be unique across all Frost Auth products. If another operator already claimed it, the save will fail.

Click **Save Changes** to store it.

> Changing an existing domain resets its verification status. You must re-verify after every change.

### Step 3 — Create the CNAME record

At your DNS provider, add a CNAME record:

| Type | Name | Value |
| - | - | - |
| `CNAME` | `api.yourdomain.com` | `cname.frostauth.cc` |

The target value (`cname.frostauth.cc`) is shown in the dashboard under the domain input. Copy it directly rather than typing it.

### Step 4 — Verify

Click the **Verify** button in the Custom Domain tab. The server performs a live DNS CNAME lookup against your hostname and checks whether it resolves to `cname.frostauth.cc`.

* **Verified** — the CNAME matches. The domain is added to the CORS allow-list within 60 seconds (the cache refresh interval).
* **Not verified yet** — the CNAME either doesn't exist yet or points somewhere else. DNS propagation can take up to an hour. Do not keep clicking verify in a loop; wait and try again.

> [!NOTE]
>
> Verification proves the CNAME points at us. It does not mean traffic will flow immediately — the edge also needs to hold a TLS certificate for the hostname before HTTPS connections succeed.

### Step 5 — Update your SDK base URL

Once verified, the dashboard shows your new SDK base URL:

```text theme={null}
https://api.yourdomain.com/api/v1
```

Replace the default API URL in your application with this value. In the official SDKs, pass it as the base URL parameter during initialization.

***

## Responses

### Save domain (`PATCH /api/v1/settings/:productId`)

```text 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "domain": "api.yourdomain.com",
      "domainVerified": false,
      "domainCheckedAt": null
    }
  }
}
```

```text 400 Invalid Format theme={null}
{
  "error": true,
  "response": {
    "code": 400,
    "message": "Enter a hostname such as api.yourdomain.com — no scheme, port or path"
  }
}
```

```text 403 Plan Required theme={null}
{
  "error": true,
  "response": {
    "code": 403,
    "message": "Custom domains need the Pro plan"
  }
}
```

```text 409 Already Taken theme={null}
{
  "error": true,
  "response": {
    "code": 409,
    "message": "That domain is already in use"
  }
}
```

```text 404 Not Found theme={null}
{
  "error": true,
  "response": {
    "code": 404,
    "message": "Product not found"
  }
}
```

```text 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Something went wrong"
  }
}
```

### Verify domain (`POST /api/v1/settings/:productId/domain/verify`)

```text 200 Verified theme={null}
{
  "success": true,
  "response": {
    "data": {
      "domain": "api.yourdomain.com",
      "domainVerified": true,
      "records": ["cname.frostauth.cc"]
    }
  }
}
```

```text 400 No Domain theme={null}
{
  "error": true,
  "response": {
    "code": 400,
    "message": "Add a domain first"
  }
}
```

```text 403 Plan Required theme={null}
{
  "error": true,
  "response": {
    "code": 403,
    "message": "Custom domains need the Pro plan"
  }
}
```

```text 409 Not Resolved theme={null}
{
  "error": true,
  "response": {
    "code": 409,
    "message": "No CNAME found on api.yourdomain.com yet — DNS can take up to an hour"
  }
}
```

```text 409 Wrong Target theme={null}
{
  "error": true,
  "response": {
    "code": 409,
    "message": "api.yourdomain.com points to something-else.com — it must point to cname.frostauth.cc"
  }
}
```

```text 404 Not Found theme={null}
{
  "error": true,
  "response": {
    "code": 404,
    "message": "Product not found"
  }
}
```

```text 429 Rate Limited theme={null}
{
  "error": true,
  "response": {
    "code": 429,
    "message": "Too many requests"
  }
}
```

```text 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Failed to verify the domain"
  }
}
```

***

## Errors

| HTTP | Message | Cause |
| - | - | - |
| `400` | `Enter a hostname such as api.yourdomain.com — no scheme, port or path` | Domain contains `://`, a port, or a path segment. |
| `400` | `Add a domain first` | Verify was called before saving a domain. |
| `403` | `Custom domains need the Pro plan` | Account is on Starter or Developer. |
| `404` | `Product not found` | Product ID doesn't exist or isn't owned by you. |
| `409` | `That domain is already in use` | Another product already claimed this hostname (platform-wide unique). |
| `409` | `No CNAME found on … yet — DNS can take up to an hour` | DNS hasn't propagated, or no CNAME exists. NXDOMAIN/SERVFAIL are treated as "not yet", not errors. |
| `409` | `… points to X — it must point to cname.frostauth.cc` | CNAME exists but targets the wrong host. |
| `429` | `Too many requests` | Verify rate limit exceeded. |
| `500` | `Failed to verify the domain` | Unhandled exception during DNS resolution or database write. |
| `500` | `Something went wrong` | Global unhandled error handler. |

***

## Behavior

* **DNS verification is a live lookup**, not a poll. Each click of Verify triggers one `resolveCname` call. There is no background polling loop.
* **Changing the domain resets verification.** Any PATCH that includes `domain` sets `domainVerified: false` and `domainCheckedAt: null`. The old domain stops being trusted on the next cache refresh (within 60 seconds).
* **CORS trust is cached.** Verified domains are held in an in-memory `Set` per instance, backed by a Redis cache with a 60-second TTL. After verification succeeds, the cache is explicitly invalidated so other instances pick it up on their next tick rather than waiting out the window.
* **Only HTTPS origins are accepted.** The CORS check parses the origin URL and refuses any protocol other than `https:`. A custom domain without TLS would send session cookies in clear text.
* **TLS certificates are separate from verification.** Verification proves the CNAME points at FrostAuth. Serving traffic also requires the edge to hold a certificate for the hostname. The edge's on-demand TLS hook calls `GET /settings/domain/issuable?domain=...` before issuing one — if your domain is verified, it answers 200 and the certificate is minted. Until that happens, HTTPS connections to your domain will fail even though the dashboard says verified.
* **Domain uniqueness is platform-wide.** Two products across different accounts cannot share the same custom domain. The database enforces this with a unique constraint.
* **Revoking a domain** (clearing the field) removes it from the verified set on the next cache refresh. Requests to the old domain will start receiving CORS refusals once the cache expires.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.