Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions evaluations/custom-domains.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"skill": "custom-domains",
"description": "Evaluation cases for the custom-domains skill. Tests whether agents produce correct DNS records, use the right registration API endpoints, and warn about common pitfalls like provider SSL interference and stale ACME records.",

"output_evals": [
{
"name": "DNS record configuration",
"prompt": "What DNS records do I need to set up to serve my canister ryjl3-tyaaa-aaaaa-aaaba-cai under app.example.com on the IC? Just the DNS records, no code or deploy steps.",
"expected_behaviors": [
"Shows a CNAME record for app.example.com pointing to app.example.com.icp1.io",
"Shows a TXT record for _canister-id.app.example.com with the canister ID ryjl3-tyaaa-aaaaa-aaaba-cai",
"Shows a CNAME record for _acme-challenge.app.example.com pointing to _acme-challenge.app.example.com.icp2.io",
"Does NOT hallucinate incorrect subdomains or record types"
]
},
{
"name": "Domain registration API",
"prompt": "I've set up my DNS records and deployed my canister with the ic-domains file. What curl commands do I run to register my custom domain app.example.com? Just the commands.",
"expected_behaviors": [
"Uses the validate endpoint: GET https://icp0.io/custom-domains/v1/app.example.com/validate",
"Uses the registration endpoint: POST https://icp0.io/custom-domains/v1/app.example.com",
"Uses the status check endpoint: GET https://icp0.io/custom-domains/v1/app.example.com",
"Does NOT invent non-existent API endpoints or parameters"
]
},
{
"name": "ic-domains file setup",
"prompt": "I have a Vite-based IC frontend project created with dfx new. Where exactly do I put the ic-domains file and what should it contain for mydomain.com? Just the file placement and content.",
"expected_behaviors": [
"Places .well-known/ic-domains inside the public/ directory, not src/",
"Lists the domain in the ic-domains file (mydomain.com)",
"Mentions .ic-assets.json5 with match .well-known and ignore false",
"Does NOT place the file in src/ where Vite would try to compile it"
]
},
{
"name": "Cloudflare SSL pitfall",
"prompt": "I'm using Cloudflare as my DNS provider for my IC custom domain. The registration keeps failing. What's likely wrong? Keep it brief.",
"expected_behaviors": [
"Identifies Cloudflare's Universal SSL as the likely culprit",
"Advises disabling Cloudflare's SSL/TLS certificate features",
"Explains that provider SSL interferes with the IC's ACME challenge"
]
},
{
"name": "Apex domain limitation",
"prompt": "I want to use example.com (no subdomain) as my custom domain on the IC, but my DNS provider won't let me set a CNAME on the apex. What are my options? Keep it brief.",
"expected_behaviors": [
"Explains that many providers don't allow CNAME on apex domains",
"Suggests using ANAME or ALIAS record types (CNAME flattening)",
"Suggests using a subdomain like www.example.com as an alternative"
]
},
{
"name": "HttpAgent host configuration",
"prompt": "My IC frontend works fine on icp0.io but API calls fail when I access it through my custom domain. What's wrong? Just the fix.",
"expected_behaviors": [
"Identifies that HttpAgent cannot auto-detect the IC API host on custom domains",
"Shows setting host to https://icp-api.io for mainnet",
"Shows HttpAgent.create({ host }) or equivalent configuration"
]
},
{
"name": "Domain update flow",
"prompt": "I want to point my existing custom domain to a different canister. What do I need to do? Just the steps.",
"expected_behaviors": [
"Updates the _canister-id TXT record to the new canister ID",
"Uses PATCH https://icp0.io/custom-domains/v1/DOMAIN to notify the service",
"Does NOT say to delete and re-register the domain"
]
},
{
"name": "Domain removal flow",
"prompt": "How do I remove a custom domain registration from the IC? Just the steps.",
"expected_behaviors": [
"Removes the _canister-id TXT and _acme-challenge CNAME DNS records",
"Uses DELETE https://icp0.io/custom-domains/v1/DOMAIN to notify the service",
"Does NOT suggest only removing DNS records without calling the API"
]
}
],

"trigger_evals": {
"description": "Queries to test whether the skill activates correctly.",
"should_trigger": [
"How do I set up a custom domain for my IC canister?",
"My custom domain registration is failing",
"What DNS records do I need for an IC custom domain?",
"How do I update my custom domain to point to a different canister?",
"How do I remove a custom domain from the IC?",
"The ACME challenge for my IC domain keeps failing",
"How do I validate my custom domain configuration?"
],
"should_not_trigger": [
"How do I deploy a frontend on the IC?",
"Set up Internet Identity for my app",
"How do I make inter-canister calls?",
"Configure .ic-assets.json5 for SPA routing",
"How do I transfer ICP tokens?",
"Deploy a Rust backend canister"
]
}
}
267 changes: 267 additions & 0 deletions skills/custom-domains/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
---
name: custom-domains
description: "Register and manage custom domains for IC canisters via the HTTP gateway custom domain service. Covers DNS record configuration (CNAME, TXT, ACME challenge), the .well-known/ic-domains file, domain registration/validation/update/deletion via the REST API, TLS certificate provisioning, and HttpAgent host configuration. Use when the user wants to serve a canister under a custom domain, configure DNS for IC, register a domain with boundary nodes, troubleshoot custom domain issues, or update/remove a custom domain. Do NOT use for general frontend hosting or asset canister configuration without custom domains — use asset-canister instead."
license: Apache-2.0
compatibility: "curl, DNS registrar access, deployed canister"
metadata:
title: "Custom Domains"
category: Frontend
---

# Custom Domains

## What This Is

By default, canisters are accessible at `<canister-id>.icp0.io`. The custom domains service lets you serve any canister under your own domain (e.g., `yourdomain.com`). You configure DNS, deploy a domain ownership file to your canister, and register via a REST API. The HTTP gateways then handle TLS certificate provisioning, renewal, and routing automatically.

## Prerequisites

- A registered domain from any registrar (e.g., Namecheap, GoDaddy, Cloudflare)
- Access to edit DNS records for that domain
- A deployed canister (typically an asset canister serving a frontend)
- `curl` for the registration API calls
- `jq` (optional, for formatting JSON responses)

## Mistakes That Break Your Setup

1. **Not disabling your DNS provider's SSL/TLS.** Providers like Cloudflare enable Universal SSL by default. This interferes with the ACME challenge the IC uses to provision certificates and can prevent certificate renewal. Disable any certificate/SSL/TLS offering from your DNS provider before registering.

2. **Setting a CNAME on the apex domain.** Many DNS providers don't allow CNAME records on the apex (e.g., `example.com` with no subdomain). Use ANAME or ALIAS record types (CNAME flattening) if your provider supports them. Otherwise, use a subdomain like `www.example.com`.

3. **Missing the `_acme-challenge` CNAME.** Without `_acme-challenge.CUSTOM_DOMAIN` pointing to `_acme-challenge.CUSTOM_DOMAIN.icp2.io`, the HTTP gateways cannot obtain a TLS certificate. Registration will fail.

4. **Multiple TXT records on `_canister-id`.** If more than one TXT record exists for `_canister-id.CUSTOM_DOMAIN`, registration fails. Keep exactly one containing your canister ID.

5. **Forgetting the `.well-known/ic-domains` file.** The canister must serve `/.well-known/ic-domains` listing your custom domain. Without it, domain ownership verification fails during registration.

6. **Not deploying `.well-known` because it's a hidden directory.** By default, `icp` / `dfx` ignores directories starting with `.`. You need a `.ic-assets.json5` file with `{ "match": ".well-known", "ignore": false }` so the directory gets included in the deployment.

7. **Stale `_acme-challenge` TXT records from your DNS provider.** Previous ACME challenges by your provider may leave TXT records on `_acme-challenge.CUSTOM_DOMAIN` that don't appear in your dashboard. These conflict with the IC's ACME flow. Disable all TLS offerings from your provider to clear them. Verify with `dig TXT _acme-challenge.CUSTOM_DOMAIN`.

8. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic.

9. **Not setting `host` in HttpAgent on custom domains.** When serving from a custom domain, the `HttpAgent` cannot automatically infer the IC API host like it can on `icp0.io`. You must set `host: "https://icp-api.io"` explicitly for mainnet.

10. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill.

## Implementation

### Step 1: Configure DNS Records

Add three DNS records (replace `CUSTOM_DOMAIN` with your domain, e.g., `app.example.com`):

| Record Type | Host | Value |
|---|---|---|
| CNAME | `CUSTOM_DOMAIN` | `CUSTOM_DOMAIN.icp1.io` |
| TXT | `_canister-id.CUSTOM_DOMAIN` | your canister ID (e.g., `hwvjt-wqaaa-aaaam-qadra-cai`) |
| CNAME | `_acme-challenge.CUSTOM_DOMAIN` | `_acme-challenge.CUSTOM_DOMAIN.icp2.io` |

Some DNS providers omit the main domain suffix. For `app.example.com` on such providers:
- `app` instead of `app.example.com`
- `_canister-id.app` instead of `_canister-id.app.example.com`
- `_acme-challenge.app` instead of `_acme-challenge.app.example.com`

For apex domains without CNAME support, use your provider's ANAME or ALIAS record type pointing to `CUSTOM_DOMAIN.icp1.io`.

### Step 2: Create the `ic-domains` File

Create `.well-known/ic-domains` in your canister's served directory. List each custom domain on its own line:

```text
app.example.com
www.example.com
```

**For Vite-based projects** (created with `dfx new` or `icp init`), place `.well-known/` inside `public/` so it gets copied to `dist/` unchanged:

```
src/project_frontend/
├── public/
│ ├── .ic-assets.json5
│ └── .well-known/
│ └── ic-domains
```

**For projects where the asset canister serves directly from source:**

```
src/project_frontend/
├── src/
│ └── .well-known/
│ └── ic-domains
```

### Step 3: Configure `.ic-assets.json5`

Place alongside the `.well-known` directory to ensure it gets deployed:

```json5
[
{
"match": ".well-known",
"ignore": false
}
]
```

If you already have a `.ic-assets.json5`, add this entry to the existing array.

### Step 4: Deploy

```bash
icp deploy frontend
```

### Step 5: Validate

Check DNS records and canister configuration before registering:

```bash
curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate" | jq
```

Success response:

```json
{
"status": "success",
"message": "Domain is eligible for registration: DNS records are valid and canister ownership is verified",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID",
"validation_status": "valid"
}
}
```

If validation fails, common errors and fixes:

| Error | Fix |
|---|---|
| Missing DNS CNAME record | Add the `_acme-challenge` CNAME pointing to `_acme-challenge.CUSTOM_DOMAIN.icp2.io` |
| Missing DNS TXT record | Add the `_canister-id` TXT record with your canister ID |
| Invalid DNS TXT record | Ensure the TXT value is a valid canister ID |
| More than one DNS TXT record | Remove duplicate `_canister-id` TXT records, keep one |
| Failed to retrieve known domains | Ensure `.well-known/ic-domains` is deployed (check `.ic-assets.json5`) |
| Domain missing from list | Add the domain to the `ic-domains` file and redeploy |

### Step 6: Register

```bash
curl -sL -X POST "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

Success response:

```json
{
"status": "success",
"message": "Domain registration request accepted and may take a few minutes to process",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID"
}
}
```

### Step 7: Wait for Certificate Provisioning

Poll until `registration_status` is `registered`:

```bash
curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

Status values: `registering` → `registered` (success), or `failed` (check error message).

After `registered`, wait a few more minutes for propagation to all HTTP gateways before testing.

## Updating a Custom Domain

To point an existing custom domain at a different canister:

1. Update the `_canister-id` TXT record to the new canister ID.
2. Notify the service:

```bash
curl -sL -X PATCH "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

3. Check status:

```bash
curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

## Removing a Custom Domain

1. Remove the `_canister-id` TXT record and `_acme-challenge` CNAME from DNS.
2. Notify the service:

```bash
curl -sL -X DELETE "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

3. Confirm deletion (should return 404):

```bash
curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq
```

## HttpAgent Configuration

On custom domains, the agent cannot auto-detect the IC API host. Set it explicitly:

```typescript
import { HttpAgent } from "@icp-sdk/core/agent";

const isProduction = process.env.NODE_ENV === "production";
const host = isProduction ? "https://icp-api.io" : undefined;
const agent = await HttpAgent.create({ host });
```

## Deploy & Test

```bash
# 1. Deploy canister with ic-domains file
icp deploy frontend

# 2. Validate DNS + canister config
curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com/validate" | jq

# 3. Register
curl -sL -X POST "https://icp0.io/custom-domains/v1/yourdomain.com" | jq

# 4. Poll until registered
curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq
```

## Verify It Works

```bash
# 1. Verify DNS records
dig CNAME yourdomain.com
# Expected: yourdomain.com. CNAME yourdomain.com.icp1.io.

dig TXT _canister-id.yourdomain.com
# Expected: "<your-canister-id>"

dig CNAME _acme-challenge.yourdomain.com
# Expected: _acme-challenge.yourdomain.com. CNAME _acme-challenge.yourdomain.com.icp2.io.

# 2. Verify ic-domains file is served by the canister
curl -sL "https://<canister-id>.icp0.io/.well-known/ic-domains"
# Expected: your domain listed

# 3. Verify registration status is "registered"
curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq '.data.registration_status'
# Expected: "registered"

# 4. Verify the custom domain serves your canister
curl -sI "https://yourdomain.com"
# Expected: HTTP/2 200

# 5. Verify no stale ACME TXT records
dig TXT _acme-challenge.yourdomain.com
# Expected: no TXT records (only the CNAME)
```
Loading