Canister Discovery
How icp-cli enables canisters to discover each other through automatic ID injection.
The Discovery Problem
Canister IDs are assigned at deployment time and differ between environments:
| Environment | Backend ID |
|---|---|
| local | bkyz2-fmaaa-aaaaa-qaaaq-cai |
| staging | rrkah-fqaaa-aaaaa-aaaaq-cai |
| ic (mainnet) | xxxxx-xxxxx-xxxxx-xxxxx-cai |
Hardcoding IDs creates problems:
- Deploying to a new environment requires code changes
- Recreating a canister invalidates hardcoded references
- Sharing code with others fails because IDs don’t match
Automatic Canister ID Injection
icp-cli solves this by automatically injecting canister IDs as canister environment variables during deployment.
How It Works
During icp deploy, icp-cli automatically:
- Collects all canister IDs in the current environment
- Creates a variable for each:
PUBLIC_CANISTER_ID:<canister-name>→<principal> - Injects all these variables into every canister in the environment
This means each canister receives the IDs of all other canisters, enabling any canister to call any other canister without hardcoding IDs.
Note: Variables are only updated for the canisters being deployed. If you deploy a single canister (
icp deploy backend), only that canister receives updated variables. When adding new canisters to an existing project, runicp deploywithout arguments to update all canisters with the complete set of IDs.
Variable Format
For an environment with backend, frontend, and worker canisters:
PUBLIC_CANISTER_ID:backend → bkyz2-fmaaa-aaaaa-qaaaq-caiPUBLIC_CANISTER_ID:frontend → bd3sg-teaaa-aaaaa-qaaba-caiPUBLIC_CANISTER_ID:worker → b77ix-eeaaa-aaaaa-qaada-caiThese variables are stored in canister settings, not baked into the WASM. The same WASM can run in different environments with different canister IDs.
Deployment Order
When deploying multiple canisters:
icp deploycreates all canisters first (getting their IDs)- Then injects
PUBLIC_CANISTER_ID:*variables into all canisters - Then installs WASM code
All canisters can reference each other’s IDs regardless of declaration order in icp.yaml.
Frontend to Backend Communication
When your frontend is deployed to an asset canister:
- The asset canister receives
PUBLIC_CANISTER_ID:*variables - It exposes them via a cookie named
ic_env, along with the network’s root key (IC_ROOT_KEY) - Your frontend JavaScript reads the cookie to get canister IDs and root key
This mechanism works identically on local networks and mainnet — your frontend code doesn’t need to change between environments.
Working Examples
- hello-world template — The template from
icp newdemonstrates this pattern. Look at the frontend source code to see how it reads the backend canister ID. - frontend-environment-variables example — A detailed example showing dev server configuration with Vite.
Implementation
Use @icp-sdk/core to read the cookie:
import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
interface CanisterEnv { "PUBLIC_CANISTER_ID:backend": string; IC_ROOT_KEY: Uint8Array; // Parsed from hex by the library}
const env = getCanisterEnv<CanisterEnv>();For local development with a dev server, see the Local Development Guide.
Backend to Backend Communication
Since all canisters receive PUBLIC_CANISTER_ID:* variables for every canister in the environment, backend canisters can discover each other’s IDs at runtime.
Reading Canister Environment Variables
Rust canisters can read the injected canister IDs using ic_cdk::api::env_var_value:
use candid::Principal;
let backend_id = Principal::from_text( &ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:backend")).unwrap();Motoko canisters can read canister environment variables using Prim.envVar (since Motoko 0.16.2):
import Prim "mo:⛔";import Principal "mo:core/Principal";
let ?backendIdText = Prim.envVar<system>("PUBLIC_CANISTER_ID:backend") else { return #err("backend canister ID not set");};let backendId = Principal.fromText(backendIdText);Note:
Primis an internal module not intended for general use. This functionality will be available in the Motoko core package in a future release.
Making Inter-Canister Calls
Once you have the target canister ID, make calls using your language’s CDK:
- Rust:
ic_cdk::callAPI - Motoko: Inter-canister calls
Alternative Patterns
If you prefer not to use canister environment variables:
- Init arguments — Pass canister IDs as initialization parameters
- Configuration — Store IDs in canister state during setup
Custom Canister Environment Variables
Beyond automatic PUBLIC_CANISTER_ID:* variables, you can define custom canister environment variables in icp.yaml. See the Environment Variables Reference for configuration syntax.
Troubleshooting
”Canister not found” errors
Ensure the target canister is deployed:
icp canister list # Check what's deployedicp deploy # Deploy all canistersCanister environment variables not available
Canister environment variables are set automatically during icp deploy. If you’re using icp canister install directly, variables won’t be set. Use icp deploy instead.
Wrong canister ID in different environment
Check which environment you’re targeting:
icp canister list -e local # Local environmenticp canister list -e production # Production environmentSee Also
- Binding Generation — Type-safe canister interfaces
- Environment Variables Reference — Complete variable documentation
- Canister Settings Reference — Settings configuration
- Build, Deploy, Sync — Deployment lifecycle details
- Local Development — Frontend local dev setup