Loading...
Loading...
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.
npx skill4agent add dfinity/icskills asset-canister@icp-sdk/canisters.icp/data/mappings/| Environment | URL Pattern |
|---|---|
| Local | |
| Mainnet | |
| Custom domain | |
sourcesource"dist""out"icp deploy.ic-assets.json5/about/aboutindex.htmlicp deploybuildsource"frontend"icp deploy assets@icp-sdk/canistersallow_raw_accessic0.appicp0.ioraw.ic0.appraw.icp0.ioallow_raw_accesstrue"allow_raw_access": false.ic-assets.json5canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
- name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/backend/main.morecipe.type: "@dfinity/asset-canister@..."icpdirbuildicp deploy.ic-assets.json5sourcedist/.ic-assets.json5sourcepublic/static/dist/[
{
// Default headers for all paths: caching, security, and raw access policy
"match": "**/*",
"security_policy": "standard",
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate"
},
// Disable raw (uncertified) access by default -- see mistake #7 above
"allow_raw_access": false
},
{
// Cache static assets aggressively (they have content hashes in filenames)
"match": "assets/**/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
// SPA fallback: serve index.html for any unmatched route
"match": "**/*",
"enable_aliasing": true
}
]"enable_aliasing": trueindex.htmlCache-ControlAccept-Encoding: gzip, bricp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec { record { "Accept-Encoding"; "gzip" } };
certificate_version = opt 2;
})'.well-known/ic-domainssourceyourdomain.com
www.yourdomain.com# CNAME record pointing to boundary nodes
yourdomain.com. CNAME icp1.io.
# ACME challenge record for TLS certificate provisioning
_acme-challenge.yourdomain.com. CNAME _acme-challenge.<your-canister-id>.icp2.io.
# Canister ID TXT record for verification
_canister-id.yourdomain.com. TXT "<your-canister-id>".well-known/ic-domains.well-known/ic-domainshttps://yourdomain.comicp deployimport { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://ic0.app";
const host = LOCAL_REPLICA; // Change to MAINNET for production
const agent = await HttpAgent.create({
host,
// Only fetch the root key when talking to a local replica.
// Setting this to true against mainnet is a security vulnerability.
shouldFetchRootKey: host === LOCAL_REPLICA,
});
const assetManager = new AssetManager({
canisterId: "your-asset-canister-id",
agent,
});
// Upload a single file
// Files >1.9MB are automatically chunked (16 parallel chunks)
const key = await assetManager.store(fileBuffer, {
fileName: "photo.jpg",
contentType: "image/jpeg",
path: "/uploads",
});
console.log("Uploaded to:", key); // "/uploads/photo.jpg"
// List all assets
const assets = await assetManager.list();
console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]
// Delete an asset
await assetManager.delete("/uploads/old-photo.jpg");
// Batch upload a directory
import { readFileSync, readdirSync } from "fs";
const files = readdirSync("./dist");
for (const file of files) {
const content = readFileSync(`./dist/${file}`);
await assetManager.store(content, { fileName: file, path: "/" });
}grant_permission--add-controller# Grant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'
# Grant commit permission -- use for deploy pipelines that need to publish assets
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'
# Grant permission management -- use for principals that need to onboard/offboard other uploaders
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'
# List current permissions
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'
# Revoke a permission
icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'Security Warning:grants full canister control -- not just upload permission. A controller can upgrade the canister WASM, change all settings, or delete the canister entirely. Only add controllers when you genuinely need full administrative access.icp canister update-settings frontend --add-controller <principal-id>
# Start the local network
icp network start -d
# Build and deploy frontend + backend
icp deploy
# Or deploy only the frontend
icp deploy frontend# Ensure you have cycles in your wallet
icp deploy -e ic frontend# Rebuild and redeploy just the frontend canister
npm run build
icp deploy frontend# 1. Check the canister is running
icp canister status frontend
# Expected: Status: Running, Memory Size: <non-zero>
# 2. List uploaded assets
icp canister call frontend list '(record {})'
# Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.
# 3. Fetch the index page via http_request
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }
# 4. Test SPA fallback (should return index.html, not 404)
icp canister call frontend http_request '(record {
url = "/about";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
# Expected: status_code = 200 (same content as "/"), NOT 404
# 5. Open in browser
# Local: http://<frontend-canister-id>.localhost:8000
# Mainnet: https://<frontend-canister-id>.ic0.app
# 6. Get canister ID
icp canister id frontend
# Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")
# 7. Check storage usage
icp canister info frontend
# Shows memory usage, module hash, controllers