Local Development
Local Development Workflow
Section titled “Local Development Workflow”The modern way to handle local development is using icp-cli and @ic-reactor/vite-plugin. This setup automates environment variables and proxy configuration.
1. Configure Vite Plugin
Section titled “1. Configure Vite Plugin”If you’re using Vite, add the plugin to your vite.config.ts. It will automatically proxy /api requests to your local replica and inject canister IDs.
import { defineConfig } from "vite"import { icReactor } from "@ic-reactor/vite-plugin"
export default defineConfig({ plugins: [ icReactor({ canisters: [ { name: "backend", didFile: "src/declarations/backend.did", }, ], }), ],})2. Set Up ClientManager
Section titled “2. Set Up ClientManager”The ClientManager automatically detects your environment through the ic_env
cookie set by the Vite plugin or an asset canister.
Put it where the generated reactors expect it. The plugin’s default
clientManagerPath is "../../clients", and its default outDir is
src/declarations, so the generated
src/declarations/backend/index.generated.ts resolves clientManager from
src/clients.ts:
import { AuthenticationManager, ClientManager, createAuthHooks,} from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
// Network and local canister IDs come from the ic_env cookie automaticallyexport const clientManager = new ClientManager({ queryClient })
export const authentication = new AuthenticationManager({ clientManager })export const { useAuth, useAgentState } = createAuthHooks(authentication)3. Run Development Cycle
Section titled “3. Run Development Cycle”-
Start the local network:
Terminal window icp network start -d -
Deploy your backend:
Terminal window icp deploy backend -
Start your dev server:
Terminal window npm run dev
Manual Configuration (Optional)
Section titled “Manual Configuration (Optional)”If you’re not using the Vite plugin, you can still configure everything manually.
1. Setup ClientManager
Section titled “1. Setup ClientManager”ClientManagerParameters has four members: the required queryClient, an
optional agentOptions (the HttpAgentOptions handed to the underlying
HttpAgent), an optional allowEnvConfig, and allowEnvRootKey — the former
name of that option, still honoured for the root key alone. Network detection needs no flag — it always
runs. allowEnvConfig controls whether the ic_env cookie is trusted at all:
its root key, its Internet Identity provider, and the canister IDs a reactor
resolves by name. It defaults to true only when both the agent host and the
page are unambiguously a local replica, and false everywhere else, so a
reactor with no configured canisterId works on localhost and throws on a
real domain. The root key is the exception: with neither option set, it is
judged by the agent host alone, so a page on a real domain pointed at a local
replica still takes IC_ROOT_KEY from the cookie. Pass allowEnvConfig: false
to refuse it.
import { ClientManager } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
// Served from localhost → the host is inferred from window.location.originexport const clientManager = new ClientManager({ queryClient })To point the agent somewhere else, pass the host explicitly:
import { ClientManager } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
export const clientManager = new ClientManager({ queryClient, agentOptions: { host: "http://127.0.0.1:4943" },})2. Configure Canister IDs
Section titled “2. Configure Canister IDs”VITE_BACKEND_CANISTER_ID=bkyz2-fmaaa-aaaaa-qaaaq-caiimport { Reactor } from "@ic-reactor/react"import { idlFactory, type _SERVICE } from "./declarations/backend"
export const backend = new Reactor<_SERVICE>({ clientManager, idlFactory, name: "backend", canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,})Codespaces and Gitpod
Section titled “Codespaces and Gitpod”A dev server forwarded by GitHub Codespaces (*.app.github.dev) or Gitpod
(*.gitpod.io) works like one on localhost: with no agentOptions.host, the
agent routes through the page’s own origin, network is "remote", and the
agent fetches the replica’s root key. Your dev server has to proxy /api to the
replica, as @ic-reactor/vite-plugin does.
The ic_env cookie is not trusted there. Every workspace is a subdomain of a
domain shared with every other user of the platform, so a page in a stranger’s
workspace can set the cookie for yours. Give reactors their canister IDs
explicitly (the codegen canisterId option), or pass allowEnvConfig: true
to ClientManager if you accept that risk for a development build:
import { ClientManager } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient()
export const clientManager = new ClientManager({ queryClient, // Codespaces/Gitpod only: trust the ic_env cookie's canister IDs, root key // and Internet Identity provider despite the shared parent domain. allowEnvConfig: import.meta.env.DEV,})Internet Identity sign-in needs a provider URL the browser can reach. Off
mainnet, AuthenticationManager derives a local one
(http://<canister>.localhost:<port>/authorize), and the plugin’s cookie
carries the same kind of URL. Neither resolves in a browser outside the
workspace, so pass identityProvider and internetIdentityId to
AuthenticationManager for an Internet Identity your browser can open (see
Authentication).
Fetching Root Key
Section titled “Fetching Root Key”When communicating with the IC mainnet, the agent uses a hardcoded public key to verify responses. For a local replica, the agent needs to fetch a different root key.
import { useEffect } from "react"import { clientManager, useAgentState } from "./clients"
function App() { const { isInitialized } = useAgentState()
useEffect(() => { clientManager.initialize() }, [])
if (!isInitialized) { return <div>Loading...</div> }
return <YourApp />}Troubleshooting
Section titled “Troubleshooting”“Fail to verify certificate”
Section titled ““Fail to verify certificate””This error usually means the agent checked a local replica’s certificate against mainnet’s root key. IC Reactor fetches the local key only when the agent host is a local (or Codespaces/Gitpod) host.
Solution: Make sure the agent points at the local replica: serve the page
from localhost/127.0.0.1 or a Codespaces or Gitpod domain, or set
agentOptions.host to the replica’s local address.
“Can’t connect to localhost:4943”
Section titled ““Can’t connect to localhost:4943””Check if your local replica is running and on which port.
-
If you use
icp-cli, runicp network status -e local --jsonand check the returned port. -
If you use a manual
dfxworkflow, rundfx info webserver-port. -
If the replica is not on
4943, point the agent at it throughagentOptions.host:import { ClientManager } from "@ic-reactor/react"import { QueryClient } from "@tanstack/react-query"export const queryClient = new QueryClient()export const clientManager = new ClientManager({queryClient,agentOptions: { host: "http://127.0.0.1:8000" },})
Internet Identity on Localhost (manual dfx workflow)
Section titled “Internet Identity on Localhost (manual dfx workflow)”If you are using a manual dfx project, deploy a local instance of the
Internet Identity canister.
-
Pull the dependency in
dfx.json:{"canisters": {//..."internet_identity": {"type": "custom","candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did","wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz","specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai"}}} -
Deploy it:
It is recommended to deploy it to the default canister ID:
Terminal window dfx deploy internet_identity --specified-id rdmx6-jaaaa-aaaaa-aaadq-cai -
Login using the local provider URL.
useAuthhandles this automatically once the local Internet Identity canister is deployed and the page is served from localhost.import { useAuth } from "./clients"function LoginButton() {// Auto-detects based on network// Local: http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943/authorize// Mainnet: https://id.ai/authorizeconst { login } = useAuth()return <button onClick={() => login()}>Login</button>}