For AI agents: Documentation index at /llms.txt

Skip to content

Project Structure

After running icp new, you have a complete project ready to build and deploy. This page explains every file and directory that icp new creates, how they fit together, and the key concepts behind the project model.

Prerequisites

You should have already created a project by following the Quickstart. The examples below use the hello-world template with a Rust backend, but the structure is similar for Motoko.

Project layout

A typical project generated by icp new my-project looks like this after the first build and deploy:

my-project/
├── icp.yaml # Project configuration (the project root)
├── .icp/ # Generated files (canister IDs, build artifacts) ← created by icp deploy
│ ├── cache/ # Ephemeral: safe to delete, rebuilt automatically
│ └── data/ # Persistent: mainnet canister ID mappings
├── backend/
│ ├── canister.yaml # Canister-specific configuration
│ ├── Cargo.toml # Rust package manifest
│ ├── backend.did # Candid interface definition
│ └── src/
│ └── lib.rs # Canister source code
├── frontend/
│ ├── canister.yaml # Asset canister configuration
│ ├── package.json # Node dependencies (binding generation)
│ └── app/ # Frontend application (React + Vite)
│ ├── src/
│ ├── dist/ # Built assets (uploaded to the asset canister) ← created by icp deploy
│ └── package.json
└── .gitignore # Ignores .icp/cache/ (but tracks .icp/data/)

icp.yaml

The icp.yaml file is the project root. icp commands look for this file in the current directory and parent directories to locate the project.

In the hello-world template, icp.yaml is minimal:

canisters:
- backend
- frontend

Each entry under canisters is a directory name. icp looks for a canister.yaml file inside that directory. You can also define canisters inline or use glob patterns:

canisters:
- canisters/* # Discover all canister.yaml files under canisters/
- name: inline-canister
build:
steps:
- type: script
commands:
- cargo build --target wasm32-unknown-unknown --release
- cp target/wasm32-unknown-unknown/release/my_canister.wasm "$ICP_WASM_OUTPUT_PATH"

Beyond canisters, icp.yaml can also define networks (where to deploy) and environments (named deployment configurations). Two of each are provided implicitly:

Implicit environmentNetworkPurpose
locallocal (managed, localhost:8000)Local development
icic (connected, https://icp-api.io)Mainnet production

You only need to add custom networks or environments when you have staging environments, testnets, or other deployment targets. See the icp-cli configuration reference for the full schema.

Canister configuration (canister.yaml)

Each canister has its own canister.yaml that defines how to build and deploy it.

Backend canister

The Motoko backend uses the @dfinity/motoko recipe:

name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/main.mo
candid: backend.did

Frontend canister

The frontend uses the @dfinity/asset-canister recipe, which builds the frontend app and uploads the output to an asset canister:

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

The build commands run in order: install dependencies, generate TypeScript bindings from the backend’s Candid file, then build the Vite app. The dir field tells the recipe which directory to upload to the asset canister.

Recipes

Recipes are reusable build templates that expand into full canister build and sync steps. Instead of writing shell commands from scratch, you reference a recipe with a version pin and pass configuration parameters.

The four official recipes cover the most common patterns:

RecipePurpose
@dfinity/rust@<version>Rust canisters with Cargo
@dfinity/motoko@<version>Motoko canisters
@dfinity/asset-canister@<version>Asset canisters for static files
@dfinity/prebuilt@<version>Pre-compiled WASM files

To see what a recipe expands to after template rendering:

Terminal window
icp project show

This outputs the effective configuration, including all expanded recipe steps and implicit defaults.

Recipes are Handlebars templates hosted at dfinity/icp-cli-recipes. You can also create local or remote recipes for custom build patterns. See the icp-cli recipes documentation for details.

The .icp/ directory

When you build or deploy, icp creates a .icp/ directory in your project root:

.icp/
├── cache/ # Ephemeral data (safe to delete)
│ ├── artifacts/ # Built WASM files
│ ├── mappings/ # Canister IDs for local networks
│ └── networks/ # Local network state
└── data/
└── mappings/ # Canister IDs for connected networks (mainnet)

What to commit

DirectoryCommit?Why
.icp/cache/NoRebuilt automatically. Add to .gitignore.
.icp/data/YesContains mainnet canister ID mappings. Deleting means icp won’t know which canisters you’ve deployed (though the canisters still exist on the network).

The hello-world template’s .gitignore already excludes .icp/cache/ and tracks .icp/data/.

Canister discovery

Canister IDs are assigned at deployment time and differ between environments. Hardcoding them creates problems when switching between local development and mainnet. icp solves this with automatic canister ID injection, triggered by icp deploy.

During deployment:

  1. All canisters are created (or looked up) to get their IDs
  2. Each canister receives environment variables for every other canister: PUBLIC_CANISTER_ID:<canister-name>
  3. WASM code is installed

Frontend reads backend IDs

The asset canister exposes injected canister IDs through a cookie called ic_env. Your frontend JavaScript reads this cookie to discover backend canister IDs at runtime, with no code changes needed between environments:

import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
interface CanisterEnv {
"PUBLIC_CANISTER_ID:backend": string;
IC_ROOT_KEY: Uint8Array;
}
const env = getCanisterEnv<CanisterEnv>();

Backend reads other backend IDs

Backend canisters read the injected variables directly:

import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";
switch (Runtime.envVar("PUBLIC_CANISTER_ID:other_canister")) {
case (?id) { Principal.fromText(id) };
case null { /* handle missing */ };
};

Binding generation

Bindings are generated TypeScript (or Rust) code that provides type-safe access to canister methods. They are created from Candid interface files (.did), which define a canister’s public API.

icp itself does not generate bindings. Instead, use dedicated tools:

LanguageToolDocumentation
TypeScript/JavaScript@icp-sdk/bindgenjs.icp.build
Rustcandid cratedocs.rs/candid
Other languagesdidc CLIgithub.com/dfinity/candid

In the hello-world template, the frontend’s build step runs npm run generate --prefix app, which uses @icp-sdk/bindgen to generate TypeScript bindings from the backend’s backend.did file.

For a deep dive on binding generation, see Binding generation.

Next steps