Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .claude/docs-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Documentation follows the Diátaxis framework:

- Schema is generated in `crates/schema-gen/`
- Referenced in `icp.yaml` files via `# yaml-language-server: $schema=...`
- Regenerate when manifest types change: `./scripts/generate-config-schema.sh`
- Regenerate when manifest types change: `./scripts/generate-config-schemas.sh`

## CLI Docs Generation

Expand All @@ -29,3 +29,37 @@ Documentation follows the Diátaxis framework:
- Consistent ordering: npm (in Quick Install), then Homebrew, then Shell Script (in Alternative Methods)
- When referencing alternatives in other docs, maintain this order: "Homebrew, shell script, ..." (e.g., "See the Installation Guide for Homebrew, shell script, or other options")
- Both `icp-cli` and `ic-wasm` are available as official Homebrew formulas: `brew install icp-cli` and `brew install ic-wasm`

## Writing Guidelines

- Use "canister environment variables" (not just "environment variables") when referring to runtime variables stored in canister settings — this distinguishes them from shell/build environment variables
- Verify code examples and CLI commands work before committing; explain non-obvious flags
- Link to anchors on other pages rather than duplicating content (e.g., `[Custom Variables](../reference/environment-variables.md#custom-variables)`)
- Link to external tools rather than duplicating their documentation

## Link Formatting

Source documentation in `docs/` must work in two contexts:

1. **GitHub**: Renders Markdown directly with `.md` extensions
2. **Starlight docs site**: `scripts/prepare-docs.sh` transforms links to clean URLs

**Link format rules:**

- Always use relative paths with `.md` extensions: `[Link](../concepts/file.md)`
- Anchors go after the extension: `[Link](../concepts/file.md#section-name)`
- Never use absolute paths or URLs for internal docs links

**Cross-reference examples:**

```markdown
# From docs/guides/local-development.md:
[Canister Discovery](../concepts/canister-discovery.md)
[Custom Variables](../reference/environment-variables.md#custom-variables)

# From docs/concepts/canister-discovery.md:
[same-directory link](binding-generation.md)
[root-level link](../tutorial.md)
```

The `prepare-docs.sh` script handles the transformation to Starlight's URL structure. If you add new link patterns, verify they work by building the docs site locally with `cd docs-site && npm run build`.
2 changes: 2 additions & 0 deletions docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export default defineConfig({
{ label: 'Project Model', slug: 'concepts/project-model' },
{ label: 'Build, Deploy, Sync', slug: 'concepts/build-deploy-sync' },
{ label: 'Environments and Networks', slug: 'concepts/environments' },
{ label: 'Canister Discovery', slug: 'concepts/canister-discovery' },
{ label: 'Binding Generation', slug: 'concepts/binding-generation' },
{ label: 'Recipes', slug: 'concepts/recipes' },
],
},
Expand Down
48 changes: 48 additions & 0 deletions docs/concepts/binding-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Binding Generation

Understanding and using type-safe client code for calling canisters.

## What Are Bindings?

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

## Candid Interface Files

Candid is the interface description language for the Internet Computer. A `.did` file defines the public methods and types a canister exposes — it's the contract between a canister and its callers.

`.did` files can be:
- **Manually authored** — Recommended for stable APIs where backward compatibility matters
- **Generated from code** — Convenient during development, but review before publishing

For Candid syntax and best practices, see the [Candid specification](https://github.com/dfinity/candid/blob/master/spec/Candid.md).

## Generating Client Bindings

icp-cli focuses on deployment — use these dedicated tools to generate bindings:

| Language | Tool | Documentation |
|----------|------|---------------|
| TypeScript/JavaScript | `@icp-sdk/bindgen` | [js.icp.build/bindgen](https://js.icp.build/bindgen) |
| Rust | `candid` crate | [docs.rs/candid](https://docs.rs/candid) |
| Other languages | `didc` CLI | [github.com/dfinity/candid](https://github.com/dfinity/candid) |

> **Note:** Generated bindings typically hardcode a canister ID or require one at initialization. With icp-cli, canister IDs differ between environments. You can look up IDs with `icp canister status <name> -i`, or read them from canister environment variables at runtime. See [Canister Discovery](canister-discovery.md) for details.

### TypeScript/JavaScript

Use `@icp-sdk/bindgen` to generate TypeScript bindings from Candid files. See the [@icp-sdk/bindgen documentation](https://js.icp.build/bindgen) for usage and build tool integration.

### Rust

The `candid` crate provides Candid serialization and code generation macros. See the [candid crate documentation](https://docs.rs/candid).

### Other Languages

The `didc` CLI generates bindings for various languages. See the [Candid repository](https://github.com/dfinity/candid) for available targets.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a note somewhere that not all generated bindings will work well with icp-cli.

  • The target canister id needs to be fixed, or
  • the target canister id has to be fetched from the environment variables.


## See Also

- [Canister Discovery](canister-discovery.md) — How canisters find each other's IDs
- [Local Development](../guides/local-development.md) — Development workflow

[Browse all documentation →](../index.md)
11 changes: 9 additions & 2 deletions docs/concepts/build-deploy-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,13 @@ The `icp deploy` command is a composite command that executes multiple steps in

1. **Build** — Compile all target canisters to WASM (always runs)
2. **Create** — Create canisters on the network (only for canisters that don't exist yet)
3. **Update Canister Environment Variables** — Apply the updated Canister Environment Variables. These include variables used by bindings allowing canister interactions.
3. **Update Canister Environment Variables** — For each canister being deployed:
- Collects IDs of all canisters in the environment
- Creates `PUBLIC_CANISTER_ID:<name>` variables for each canister
- Merges with any custom `environment_variables` from settings
- Updates canister settings via the Management Canister

This step enables canisters to discover each other without hardcoding IDs. See [Canister Discovery](canister-discovery.md) for details.
4. **Update Settings** — Apply canister settings (controllers, memory allocation, compute allocation, etc.)
5. **Install** — Install WASM code into canisters (always runs)
6. **Sync** — Run post-deployment steps like asset uploads (only if sync steps are configured)
Expand All @@ -151,7 +157,7 @@ The `icp deploy` command is a composite command that executes multiple steps in

**Subsequent deployments:**
- Skip the canister creation
- Settings and Environment Variables are applied if they've changed.
- Settings and Canister Environment Variables are applied if they've changed.
- WASM code is upgraded, preserving canister state

Unlike `icp canister create` (which prints "already exists" and exits), `icp deploy` silently skips creation for existing canisters and continues with the remaining steps.
Expand Down Expand Up @@ -209,5 +215,6 @@ icp sync
## Next Steps

- [Local Development](../guides/local-development.md) — Apply this in practice
- [Canister Discovery](canister-discovery.md) — How canisters discover each other

[Browse all documentation →](../index.md)
171 changes: 171 additions & 0 deletions docs/concepts/canister-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 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](../reference/environment-variables.md#canister-runtime-environment-variables) during deployment.

### How It Works

During `icp deploy`, icp-cli automatically:

1. Collects all canister IDs in the current environment
2. Creates a variable for each: `PUBLIC_CANISTER_ID:<canister-name>` → `<principal>`
3. 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, run `icp deploy` without 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-cai
PUBLIC_CANISTER_ID:frontend → bd3sg-teaaa-aaaaa-qaaba-cai
PUBLIC_CANISTER_ID:worker → b77ix-eeaaa-aaaaa-qaada-cai
```

These 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:

1. `icp deploy` creates all canisters first (getting their IDs)
2. Then injects `PUBLIC_CANISTER_ID:*` variables into all canisters
3. 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:

1. The asset canister receives `PUBLIC_CANISTER_ID:*` variables
2. It exposes them via a cookie named `ic_env`, along with the network's root key (`IC_ROOT_KEY`)
3. 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 new` demonstrates this pattern. Look at the frontend source code to see how it reads the backend canister ID.
- **[frontend-environment-variables example](https://github.com/dfinity/icp-cli/tree/main/examples/icp-frontend-environment-variables)** — A detailed example showing dev server configuration with Vite.

### Implementation

Use [@icp-sdk/core](https://www.npmjs.com/package/@icp-sdk/core) to read the cookie:

```typescript
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](../guides/local-development.md#frontend-development).

## 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`](https://docs.rs/ic-cdk/latest/ic_cdk/api/fn.env_var_value.html):

```rust
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):

```motoko
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:** `Prim` is 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::call`](https://docs.rs/ic-cdk/latest/ic_cdk/call/index.html) API
- **Motoko**: [Inter-canister calls](https://docs.internetcomputer.org/motoko/fundamentals/actors/messaging#inter-canister-calls)

### Alternative Patterns

If you prefer not to use canister environment variables:

1. **Init arguments** — Pass canister IDs as initialization parameters
2. **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](../reference/environment-variables.md#custom-variables) for configuration syntax.

## Troubleshooting

### "Canister not found" errors

Ensure the target canister is deployed:

```bash
icp canister list # Check what's deployed
icp deploy # Deploy all canisters
```

### Canister 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:

```bash
icp canister list -e local # Local environment
icp canister list -e production # Production environment
```

## See Also

- [Binding Generation](binding-generation.md) — Type-safe canister interfaces
- [Environment Variables Reference](../reference/environment-variables.md) — Complete variable documentation
- [Canister Settings Reference](../reference/canister-settings.md) — Settings configuration
- [Build, Deploy, Sync](build-deploy-sync.md) — Deployment lifecycle details
- [Local Development](../guides/local-development.md) — Frontend local dev setup

[Browse all documentation →](../index.md)
2 changes: 2 additions & 0 deletions docs/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Understanding how icp-cli organizes and manages your project.
- [Build, Deploy, Sync](build-deploy-sync.md) — The three phases of the deployment lifecycle
- [Environments and Networks](environments.md) — Deployment targets and how they relate
- [Recipes](recipes.md) — Templated, reusable build configurations
- [Canister Discovery](canister-discovery.md) — How canisters discover each other
- [Binding Generation](binding-generation.md) — Type-safe canister interfaces

## Quick Reference

Expand Down
Loading
Loading