Skip to content

Latest commit

 

History

History
377 lines (289 loc) · 12.1 KB

File metadata and controls

377 lines (289 loc) · 12.1 KB
title Frontend frameworks
description Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using the asset canister and icp-cli
sidebar
order
5

ICP hosts frontend applications as asset canisters: static files (HTML, CSS, JavaScript) deployed to the network 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

  • 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

Every frontend framework integration follows the same pattern:

  1. Configure icp.yaml to point at your framework's build output directory
  2. Optionally add a Vite plugin (@icp-sdk/bindgen) to generate typed canister bindings at build time
  3. Use @icp-sdk/core in your app to read canister IDs and the root key at runtime from the ic_env cookie served by the asset canister
  4. 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

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

canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      configuration:
        build:
          - npm install
          - npm run generate --prefix app
          - npm run build
        dir: app/dist

The 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

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/core
npm install -D @icp-sdk/bindgen @vitejs/plugin-react

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

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

Vue and Vite follow the same pattern as React. The only difference is the Vite plugin:

npm install @icp-sdk/core
npm install -D @icp-sdk/bindgen @vitejs/plugin-vue
// vite.config.ts
import { 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

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

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

npm install -D @sveltejs/adapter-static
// svelte.config.js
import adapter from "@sveltejs/adapter-static";

export default {
  kit: {
    adapter: adapter({
      pages: "build",
      assets: "build",
      fallback: "index.html", // enables SPA mode
    }),
  },
};
# icp.yaml
canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      configuration:
        build:
          - npm install
          - npm run build
        dir: build

For 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

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:

// next.config.js
const nextConfig = {
  output: "export",
};

module.exports = nextConfig;

This outputs static files to the out/ directory.

# icp.yaml
canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      configuration:
        build:
          - npm install
          - npm run build
        dir: out

:::note Only Next.js pages that can be statically generated are compatible with ICP. Any page using dynamic server-side features (server actions, route handlers, middleware) will not work in a static export. :::

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

Export your game from Unity Editor: File → Build Settings → WebGL → Build. This creates a folder with index.html, Build/, and TemplateData/.

# icp.yaml
canisters:
  - name: unity_webgl_template_assets
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      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

Export your game from Godot Editor: Project → Export → HTML5 → Export Project. This creates an index.html and supporting files.

# icp.yaml
canisters:
  - name: godot_html5_assets
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      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

Both game engine templates deploy with standard icp-cli commands:

# Start local network
icp network start -d

# Deploy the asset canister
icp deploy

# Access your game locally
# http://<canister-id>.localhost:8000

No 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

For sites with no backend canister (portfolios, landing pages, documentation):

# icp.yaml
canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.2.1"
      configuration:
        build:
          - npm install
          - npm run build
        dir: dist

No 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

# Start local network
icp network start -d

# Deploy all canisters
icp deploy

# Deploy to mainnet
icp deploy -e ic

After deployment, the asset canister URL depends on your canister ID:

Environment URL
Local http://<canister-id>.localhost:8000
Mainnet https://<canister-id>.icp.net

Get your canister ID with:

icp canister settings show frontend -i

Next steps