Frontend Frameworks
ICP hosts frontend applications as asset canisters: static files (HTML, CSS, JavaScript) deployed onchain and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot.
This guide shows you how to configure your framework’s build pipeline, wire up the ICP JavaScript SDK, and deploy to an asset canister.
Prerequisites
Section titled “Prerequisites”- icp-cli installed:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - A backend canister deployed (or a static-only site with no backend)
- Familiarity with asset canisters
The deployment model
Section titled “The deployment model”Every frontend framework integration follows the same pattern:
- Configure
icp.yamlto point at your framework’s build output directory - Optionally add a Vite plugin (
@icp-sdk/bindgen) to generate typed canister bindings at build time - Use
@icp-sdk/corein your app to read canister IDs and the root key at runtime from theic_envcookie served by the asset canister - Deploy with
icp deploy
The asset canister injects an ic_env cookie into every HTML response. This cookie carries the root key and any PUBLIC_CANISTER_ID:<name> environment variables you set: so your frontend never needs canister IDs baked into the build artifact.
React with Vite
Section titled “React with Vite”The hello-world template uses React with Vite. It demonstrates the fullstack: backend canister, auto-generated TypeScript bindings, and a React frontend that reads canister IDs at runtime.
icp.yaml
Section titled “icp.yaml”canisters: - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: - npm install - npm run generate --prefix app - npm run build dir: app/distThe build array runs before the asset canister uploads files. npm run generate regenerates TypeScript bindings from the backend .did file; npm run build runs Vite.
vite.config.ts
Section titled “vite.config.ts”import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
// Change these values to match your local replica.// The `icp network start` command prints the root key;// the `icp deploy` command prints the backend canister ID.const IC_ROOT_KEY_HEX = "<IC_ROOT_KEY_HEX>";const BACKEND_CANISTER_ID = "<BACKEND_CANISTER_ID>";
export default defineConfig({ plugins: [ react(), icpBindgen({ didFile: "../../backend/backend.did", outDir: "./src/backend/api", }), ], server: { headers: { // Simulate the ic_env cookie that the asset canister injects in production. "Set-Cookie": `ic_env=${encodeURIComponent( `ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}` )}; SameSite=Lax;`, }, proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true, }, }, },});The icpBindgen Vite plugin regenerates TypeScript bindings whenever the .did file changes during development.
The server.headers block simulates the ic_env cookie during vite dev. In production, the asset canister injects this cookie automatically: your code reads it without any build-time environment variables.
Install the required packages:
npm install @icp-sdk/corenpm install -D @icp-sdk/bindgen @vitejs/plugin-reactReading canister IDs and the root key
Section titled “Reading canister IDs and the root key”import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";import { createActor } from "./backend/api/backend";
interface CanisterEnv { readonly "PUBLIC_CANISTER_ID:backend": string;}
// Reads from the ic_env cookie injected by the asset canister (production)// or the Set-Cookie header set in vite.config.ts (development).const canisterEnv = getCanisterEnv<CanisterEnv>();const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"];
const actor = createActor(canisterId, { agentOptions: { // In production, use the root key from the ic_env cookie. // In development (import.meta.env.DEV), fetch it from the local replica. rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined, shouldFetchRootKey: import.meta.env.DEV, },});The createActor function is generated by @icp-sdk/bindgen from your .did file. It returns a fully typed actor. See the JS SDK docs for the full HttpAgent and Actor API.
SPA routing
Section titled “SPA routing”React apps use client-side routing. Without a fallback, refreshing on /about returns a 404 from the asset canister. Add a .ic-assets.json5 file to your public/ directory so it ends up in dist/:
[ { // Apply security policy to all paths. Two separate rules are needed because // `security_policy` and `enable_aliasing` interact: the aliasing rule must // be evaluated last so it only applies to paths with no matching file. "match": "**/*", "security_policy": "standard", "allow_raw_access": false }, { // SPA fallback: serve index.html for any path that has no matching file. "match": "**/*", "enable_aliasing": true }]See asset canister configuration for the full .ic-assets.json5 reference.
Vue with Vite
Section titled “Vue with Vite”Vue and Vite follow the same pattern as React. The only difference is the Vite plugin:
npm install @icp-sdk/corenpm install -D @icp-sdk/bindgen @vitejs/plugin-vueimport { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
export default defineConfig({ plugins: [ vue(), icpBindgen({ didFile: "../backend/backend.did", outDir: "./src/backend/api", }), ], server: { proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, }, },});If your Vue app calls getCanisterEnv() to read canister IDs, add the same server.headers block from the React section to simulate the ic_env cookie during local development (otherwise getCanisterEnv() will throw because the cookie is absent. The icp.yaml configuration is the same as the React example) point dir at dist.
Authentication
Section titled “Authentication”Authentication with Internet Identity is framework-agnostic. The @icp-sdk/auth package works the same way in React, Vue, Svelte, and Next.js static export mode. See the Internet Identity guide for integration steps.
Svelte and SvelteKit
Section titled “Svelte and SvelteKit”For SvelteKit, you must configure static export mode before deploying. The asset canister serves static files and does not support server-side rendering.
SvelteKit with static adapter
Section titled “SvelteKit with static adapter”npm install -D @sveltejs/adapter-staticimport adapter from "@sveltejs/adapter-static";
export default { kit: { adapter: adapter({ pages: "build", assets: "build", fallback: "index.html", // enables SPA mode }), },};canisters: - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: - npm install - npm run build dir: buildFor Svelte (without SvelteKit), Vite is the standard build tool. The vite.config.js setup is the same as Vue: swap @vitejs/plugin-vue for @sveltejs/vite-plugin-svelte.
Next.js
Section titled “Next.js”Next.js requires static export mode. Server components, API routes, and getServerSideProps are not supported in an asset canister. The canister only serves static files.
Enable static export in your Next.js config:
const nextConfig = { output: "export",};
module.exports = nextConfig;This outputs static files to the out/ directory.
canisters: - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: - npm install - npm run build dir: outGame engines
Section titled “Game engines”Game engines that export HTML5 or WebGL builds can be deployed as asset canisters without a backend canister. The build output is pre-generated in the export step of the engine: icp.yaml just copies the files into place.
Unity WebGL
Section titled “Unity WebGL”Export your game from Unity Editor: File → Build Settings → WebGL → Build. This creates a folder with index.html, Build/, and TemplateData/.
canisters: - name: unity_webgl_template_assets recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: dir: dist build: - mkdir -p dist - cp -r src/unity_webgl_template_assets/assets/* dist/ - cp -r src/unity_webgl_template_assets/src/* dist/The build commands copy the Unity WebGL export into dist/. Point dir at that directory.
See the Unity WebGL example for the full project structure.
Godot HTML5
Section titled “Godot HTML5”Export your game from Godot Editor: Project → Export → HTML5 → Export Project. This creates an index.html and supporting files.
canisters: - name: godot_html5_assets recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: dir: dist build: - mkdir -p dist - cp -r src/godot_html5_assets/assets/* dist/ - cp -r src/godot_html5_assets/src/* dist/See the Godot HTML5 example for the full project structure.
Deploying game builds
Section titled “Deploying game builds”Both game engine templates deploy with standard icp-cli commands:
# Start local networkicp network start -d
# Deploy the asset canistericp deploy
# Access your game locally# http://<canister-id>.localhost:8000No Vite plugin or JS SDK integration is needed for game builds. The asset canister serves the pre-built HTML and JavaScript files directly.
Static sites
Section titled “Static sites”For sites with no backend canister (portfolios, landing pages, documentation):
canisters: - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: build: - npm install - npm run build dir: distNo JS SDK integration is needed. The asset canister serves your files, and you can configure headers and caching in .ic-assets.json5.
See the React hosting example for a minimal static frontend without a backend canister.
Deploy
Section titled “Deploy”# Start local networkicp network start -d
# Deploy all canistersicp deploy
# Deploy to mainneticp deploy -e icAfter deployment, the asset canister URL depends on your canister ID:
| Environment | URL |
|---|---|
| Local | http://<canister-id>.localhost:8000 |
| Mainnet | https://<canister-id>.ic0.app |
Get your canister ID with:
icp canister settings show frontend -iNext steps
Section titled “Next steps”- Asset canister: configure headers, caching, and SPA routing in
.ic-assets.json5 - Internet Identity: add authentication to your frontend
- Project structure: explore the hello-world template with React, Vite, and a Motoko backend