From eab73b3b9412f92c2ce2cf5724c942f4bb9dce7a Mon Sep 17 00:00:00 2001 From: Andrew Battat Date: Wed, 25 Mar 2026 21:03:31 +0000 Subject: [PATCH 1/4] Custom domain skill --- skills/custom-domains/SKILL.md | 267 +++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 skills/custom-domains/SKILL.md diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md new file mode 100644 index 0000000..c103f03 --- /dev/null +++ b/skills/custom-domains/SKILL.md @@ -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 `.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: "" + +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://.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) +``` From 3fe8b832dda891dcd4b305fc0b27924412411afa Mon Sep 17 00:00:00 2001 From: Andrew Battat Date: Wed, 25 Mar 2026 21:15:05 +0000 Subject: [PATCH 2/4] Add custom-domains.json --- evaluations/custom-domains.json | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 evaluations/custom-domains.json diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json new file mode 100644 index 0000000..f01d086 --- /dev/null +++ b/evaluations/custom-domains.json @@ -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" + ] + } +} From 40bc629c658663fdf355d0bc9d51c241d26f7fae Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 31 Mar 2026 10:08:01 +0200 Subject: [PATCH 3/4] chore: make custom-domains canister-agnostic and clean up trigger separation - custom-domains skill now acknowledges it works with any HTTP-serving canister, not just asset canisters - asset-canister description and body trimmed to cross-reference custom-domains instead of duplicating setup instructions - added should_not_trigger entries to both eval files for clean routing --- evaluations/asset-canister.json | 4 +++- evaluations/custom-domains.json | 4 +++- skills/asset-canister/SKILL.md | 37 ++------------------------------- skills/custom-domains/SKILL.md | 4 +++- 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/evaluations/asset-canister.json b/evaluations/asset-canister.json index dc8f6b9..7dfcf98 100644 --- a/evaluations/asset-canister.json +++ b/evaluations/asset-canister.json @@ -60,7 +60,9 @@ "Set up stable memory in Motoko", "Connect a wallet to my dapp", "How do I create an ICRC-1 token?", - "Deploy a Rust backend canister" + "Deploy a Rust backend canister", + "How do I set up a custom domain on the IC?", + "Configure DNS for my IC canister" ] } } diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index f01d086..558d122 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -97,7 +97,9 @@ "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" + "Deploy a Rust backend canister", + "Configure .ic-assets.json5 for my project", + "How do I upload files to an asset canister programmatically?" ] } } diff --git a/skills/asset-canister/SKILL.md b/skills/asset-canister/SKILL.md index d5d4f99..4101db7 100644 --- a/skills/asset-canister/SKILL.md +++ b/skills/asset-canister/SKILL.md @@ -1,6 +1,6 @@ --- name: asset-canister -description: "Deploy frontend assets to the IC. Covers certified assets, SPA routing with .ic-assets.json5, custom domains, content encoding, and programmatic uploads. Use when hosting a frontend, deploying static files, configuring custom domains, or setting up SPA routing on IC. Do NOT use for canister-level code patterns." +description: "Deploy frontend assets to the IC. Covers certified assets, SPA routing with .ic-assets.json5, content encoding, and programmatic uploads. Use when hosting a frontend, deploying static files, or setting up SPA routing on IC. Do NOT use for canister-level code patterns or custom domain setup — use custom-domains instead." license: Apache-2.0 compatibility: "icp-cli >= 0.1.0, Node.js >= 22" metadata: @@ -122,40 +122,7 @@ icp canister call frontend http_request '(record { ### Custom Domain Setup -To serve your asset canister from a custom domain: - -1. Create a file `.well-known/ic-domains` in your `dir` directory containing your domain: -```text -yourdomain.com -www.yourdomain.com -``` - -2. Add DNS records: -```text -# CNAME record pointing to boundary nodes -yourdomain.com. CNAME yourdomain.com.icp1.io. - -# ACME challenge record for TLS certificate provisioning -_acme-challenge.yourdomain.com. CNAME _acme-challenge.yourdomain.com.icp2.io. - -# Canister ID TXT record for verification -_canister-id.yourdomain.com. TXT "" -``` - -3. Deploy your canister so the `.well-known/ic-domains` file is available. - -4. Validate that DNS records and canister ownership are correct: -```bash -curl -sL -X GET https://icp0.io/custom-domains/v1/yourdomain.com/validate | jq -# Expected: { "status": "success", "message": "Domain is eligible for registration: DNS records are valid and canister ownership is verified", ... } -``` - -5. Register the domain with the boundary nodes (required — registration is NOT automatic): -```bash -curl -sL -X POST https://icp0.io/custom-domains/v1/yourdomain.com | jq -``` - -6. Wait for the boundary nodes to provision the TLS certificate. This typically takes a few minutes. Verify by visiting `https://yourdomain.com` once DNS has propagated. +For custom domain setup (DNS configuration, TLS certificates, domain registration via the REST API), see the `custom-domains` skill. The only asset-canister-specific detail: your `.well-known/ic-domains` file must be in your `dir` directory so it gets deployed. Add `{ "match": ".well-known", "ignore": false }` to your `.ic-assets.json5` to ensure the hidden directory is included. ### Programmatic Uploads with @icp-sdk/canisters diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index c103f03..0f1a7d7 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -14,11 +14,13 @@ metadata: By default, canisters are accessible at `.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. +Custom domains work at the boundary node level — they map a domain to any canister ID via DNS. This works with any canister that can serve `/.well-known/ic-domains` over HTTP, not just asset canisters. That includes asset canisters, Juno satellites, and custom canisters implementing `http_request`. + ## 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) +- A deployed canister that serves `/.well-known/ic-domains` over HTTP (asset canisters, Juno satellites, or any canister implementing `http_request`) - `curl` for the registration API calls - `jq` (optional, for formatting JSON responses) From 495a0c1326030a72364583c25634711169fb5e82 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 31 Mar 2026 10:56:40 +0200 Subject: [PATCH 4/4] chore: generalize custom-domains skill and refine evals - Remove asset-canister-specific content (pitfall #6, .ic-assets.json5 step, Vite directory layouts) from custom-domains skill - Replace with canister-agnostic instructions and scoped notes for asset canister vs custom http_request canisters - Generalize deploy steps to not prescribe specific commands - Rewrite ic-domains eval to be canister-agnostic - Adjust expected behaviors to test for actionable guidance rather than keyword matching --- evaluations/custom-domains.json | 12 +++---- skills/custom-domains/SKILL.md | 63 ++++++++------------------------- 2 files changed, 20 insertions(+), 55 deletions(-) diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index 558d122..813668f 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -25,12 +25,12 @@ }, { "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.", + "prompt": "What file does my canister need to serve for custom domain verification, and what should it contain for mydomain.com? Keep it brief.", "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" + "Identifies the file as .well-known/ic-domains", + "States the canister must serve /.well-known/ic-domains", + "Lists the domain in the file (mydomain.com)", + "Does NOT assume a specific canister type or framework" ] }, { @@ -46,7 +46,7 @@ "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", + "Provides solutions for the CNAME-on-apex limitation", "Suggests using ANAME or ALIAS record types (CNAME flattening)", "Suggests using a subdomain like www.example.com as an alternative" ] diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index 0f1a7d7..5e9350b 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -36,15 +36,13 @@ Custom domains work at the boundary node level — they map a domain to any cani 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. +6. **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`. -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`. +7. **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. -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. +8. **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. -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. +9. **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 @@ -67,54 +65,22 @@ For apex domains without CNAME support, use your provider's ANAME or ALIAS recor ### 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: +Your canister must serve `/.well-known/ic-domains` over HTTP. Create this file listing 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 -``` +**Asset canister users:** place `.well-known/` inside your `public/` directory (Vite projects) or alongside your source files, and ensure `.ic-assets.json5` includes `{ "match": ".well-known", "ignore": false }` so the hidden directory gets deployed. See the `asset-canister` skill for details on file placement. -### Step 3: Configure `.ic-assets.json5` +**Custom `http_request` canisters:** serve the file contents at `/.well-known/ic-domains` directly from your HTTP request handler. -Place alongside the `.well-known` directory to ensure it gets deployed: +### Step 3: Deploy -```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 -``` +Deploy your canister so that `/.well-known/ic-domains` is accessible at `https://.icp0.io/.well-known/ic-domains`. -### Step 5: Validate +### Step 4: Validate Check DNS records and canister configuration before registering: @@ -144,10 +110,10 @@ If validation fails, common errors and fixes: | 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`) | +| Failed to retrieve known domains | Ensure `.well-known/ic-domains` is deployed and served by the canister | | Domain missing from list | Add the domain to the `ic-domains` file and redeploy | -### Step 6: Register +### Step 5: Register ```bash curl -sL -X POST "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq @@ -166,7 +132,7 @@ Success response: } ``` -### Step 7: Wait for Certificate Provisioning +### Step 6: Wait for Certificate Provisioning Poll until `registration_status` is `registered`: @@ -225,8 +191,7 @@ const agent = await HttpAgent.create({ host }); ## Deploy & Test ```bash -# 1. Deploy canister with ic-domains file -icp deploy frontend +# 1. Deploy your canister with the ic-domains file served at /.well-known/ic-domains # 2. Validate DNS + canister config curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com/validate" | jq