From 1e61efffeeb80f4fabf6419b5c452476d11e90ba Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 13 Mar 2026 10:16:02 +0100 Subject: [PATCH 1/3] docs: application architecture concept page Explain how ICP applications are structured, covering the default two-canister model, comparison with traditional web apps and Ethereum, four architectural patterns (single, per-service, per-subnet, per-user), data storage, and frontend options. Co-Authored-By: Claude Opus 4.6 --- docs/concepts/app-architecture.md | 136 +++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 11 deletions(-) diff --git a/docs/concepts/app-architecture.md b/docs/concepts/app-architecture.md index 82cc1889..88933ac3 100644 --- a/docs/concepts/app-architecture.md +++ b/docs/concepts/app-architecture.md @@ -6,17 +6,131 @@ sidebar: icskills: [multi-canister] --- -TODO: Write content for this page. +An application on the Internet Computer typically consists of one or more [canisters](canisters.md) that handle backend logic, store data, and optionally serve a web frontend — all without external servers, databases, or CDNs. This page explains how these pieces fit together and what architectural patterns are available as your application grows. - -Explain how ICP applications are structured. Cover the canister-as-backend model, frontend-to-canister flow (HTTP requests to canisters via boundary nodes), inter-canister communication, and multi-canister architectures. Compare with traditional web apps (ICP replaces server + database + CDN) and Ethereum (ICP has richer execution, serves HTTP, persists state). Include a "coming from Ethereum" callout mapping concepts. +## The default two-canister model - -- Portal: building-apps/best-practices/application-architectures.mdx, getting-started/app-architecture.mdx -- Learn Hub: [ICP and the Internet](https://learn.internetcomputer.org/hc/en-us/articles/34574399808788), [ICP Edge Infrastructure](https://learn.internetcomputer.org/hc/en-us/articles/34212818609684), [ICP subsystems](https://learn.internetcomputer.org/hc/en-us/articles/44549459496596) +Most ICP applications start with two canisters: - -- concepts/canisters -- canister details -- guides/frontends/asset-canister -- frontend deployment -- guides/canister-calls/onchain-calls -- canister-to-canister communication -- concepts/network-overview -- network layer +- **Backend canister** — contains your application logic and data. You write it in Motoko, Rust, TypeScript, or Python, and the network compiles and runs it as WebAssembly. +- **Frontend (asset) canister** — serves your web UI. It is a special system-provided canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP. + +When a user opens your application in a browser: + +1. The browser sends an HTTPS request to a [boundary node](network-overview.md). +2. The boundary node routes the request to the frontend canister, which returns the HTML and JavaScript. +3. The JavaScript uses an agent library (like `@dfinity/agent`) to send messages to the backend canister. +4. The backend canister processes the message, updates its state if needed, and returns a response. +5. The frontend renders the result. + +This flow replaces the traditional web stack. There is no separate web server, application server, or database — the backend canister handles all three roles, and the frontend canister replaces your CDN. + +## How ICP compares to traditional architectures + +| Concern | Traditional web app | ICP application | +|---------|-------------------|-----------------| +| **Compute** | Application server (Node, Django, etc.) | Backend canister (Wasm) | +| **Storage** | Database (Postgres, MongoDB, etc.) | Canister stable memory (up to 500 GiB) | +| **Frontend hosting** | CDN + static file server | Asset canister | +| **Authentication** | OAuth provider or custom auth | [Internet Identity](../guides/authentication/internet-identity.md) (passkey-based) | +| **Scheduled tasks** | Cron jobs, worker queues | Canister timers | +| **External API calls** | Server-side HTTP requests | [HTTPS outcalls](https-outcalls.md) | +| **Infrastructure management** | You manage servers, scaling, uptime | The network handles replication and availability | + +The key difference: ICP applications are self-contained. You deploy code and data to canisters, and the network provides compute, storage, and serving. There is no infrastructure to provision or maintain. + +## Coming from Ethereum + +If you have built on Ethereum or other EVM chains, here is how ICP concepts map: + +| Ethereum | ICP | Key difference | +|----------|-----|----------------| +| Smart contract | [Canister](canisters.md) | Canisters hold GiBs of state, serve HTTP, run Wasm | +| EVM bytecode | WebAssembly | Wasm runs general-purpose code at near-native speed | +| Solidity / Vyper | Motoko, Rust, TypeScript, Python | Multiple language options, full standard libraries | +| Block time (~12s) | Finality (~1–2s) | Update calls finalize in 1–2 seconds | +| Gas (user pays) | [Cycles](reverse-gas-model.md) (canister pays) | Users interact for free; developers fund computation | +| No HTTP serving | Built-in HTTP serving | Canisters serve web pages directly | +| Off-chain storage (IPFS, etc.) | On-chain stable memory | Up to 500 GiB per canister, no external storage needed | +| Bridges / oracles | [Chain-key signing](chain-fusion.md) | Canisters sign transactions on other chains natively | +| Immutable by default | Upgradeable by default | Canisters can be upgraded while preserving state | + +The biggest shift: on Ethereum, smart contracts are minimal programs that rely on off-chain infrastructure for anything beyond basic state transitions. On ICP, a canister can be an entire application — frontend, backend, database, and scheduled jobs — all on-chain. + +## Architectural patterns + +As your application grows, you can choose from several patterns. Start simple and evolve as needed — over-architecting from the start is a common mistake. + +### Single canister + +Everything — assets, logic, and data — lives in one canister. This is the simplest architecture and works well for applications serving up to thousands of users. + +**When to use:** prototypes, personal tools, simple dapps, or applications where data and compute fit comfortably within a single canister's limits. + +### Canister-per-service (default) + +Separate canisters handle distinct responsibilities. The default two-canister setup (frontend + backend) is the simplest form. You can add more canisters as responsibilities grow: one for user data, one for content, one for payments. + +**When to use:** most production applications. This is the recommended starting point. + +**Things to know:** +- Inter-canister calls are asynchronous. Code before and after an `await` executes in separate message rounds — this affects atomicity. +- Request and response payloads are limited to 2 MiB per call. +- Cross-subnet calls add one consensus round of latency compared to same-subnet calls. + +For implementation details and common pitfalls, see [Onchain calls](../guides/canister-calls/onchain-calls.md). + +### Canister-per-subnet + +For maximum throughput, distribute canisters across multiple [subnets](network-overview.md). Each subnet processes messages independently, so spreading load across subnets lets your application scale horizontally. + +**When to use:** high-throughput applications that exceed what a single subnet can handle (thousands of concurrent users, heavy computation). + +**Trade-offs:** cross-subnet calls have higher latency and bandwidth limits. You need to design data partitioning carefully. + +### Canister-per-user + +Each user gets their own canister, giving them direct control over their data. A factory canister creates and manages individual user canisters. + +**When to use:** applications where users need full sovereignty over their data and compute. This is the most complex pattern and is still an emerging area. + +**Trade-offs:** managing thousands of canisters requires careful cycle management and upgrade coordination. The factory canister must provision enough cycles for each new canister. + +## Data storage + +Canisters store data in [stable memory](../guides/backends/stable-memory.md) — there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory: + +- **Motoko:** the [`core` standard library](https://mops.one/core/docs) includes persistent data structures that survive upgrades automatically. +- **Rust:** [`ic-stable-structures`](https://docs.rs/ic-cdk/latest/ic_cdk/) provides `StableBTreeMap` and other structures for stable memory. + +For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the [canister-per-service](#canister-per-service-default) or [canister-per-subnet](#canister-per-subnet) patterns to distribute storage across canisters. + +## Frontend options + +Not every ICP application needs the default asset canister. Your options: + +- **Asset canister** — the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to an asset canister that serves it over HTTP. See [Asset canister](../guides/frontends/asset-canister.md). +- **Framework-specific canister** — use a framework like Juno that provides a more opinionated hosting solution on ICP. +- **Off-chain frontend** — host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using `@dfinity/agent`. Useful during migration or when you need features that asset canisters don't support. +- **No frontend** — backend-only canisters that expose a Candid API for other canisters or CLI tools to call. + +## Choosing an architecture + +| Question | If yes | If no | +|----------|--------|-------| +| Is this a prototype or simple tool? | [Single canister](#single-canister) | Keep reading | +| Does the app have a web UI? | Add an [asset canister](#the-default-two-canister-model) | Backend-only canister | +| Do you need to separate concerns (auth, storage, compute)? | [Canister-per-service](#canister-per-service-default) | Stick with two canisters | +| Do you need to scale beyond one subnet? | [Canister-per-subnet](#canister-per-subnet) | Stay on one subnet | +| Do users need sovereignty over their data? | [Canister-per-user](#canister-per-user) | Use shared canisters | + +Start with the simplest architecture that meets your requirements. You can always split a canister into multiple canisters later — it is much harder to merge canisters that were split prematurely. + +## What's next + +- [Quickstart](../getting-started/quickstart.md) — deploy your first application +- [Onchain calls](../guides/canister-calls/onchain-calls.md) — inter-canister communication patterns +- [Asset canister](../guides/frontends/asset-canister.md) — frontend deployment +- [Canisters](canisters.md) — canister internals + + From 1d9028737aef11a531131b0be61ae3ebda31cdae Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 13 Mar 2026 12:09:06 +0100 Subject: [PATCH 2/3] fix: address PR #4 review feedback --- docs/concepts/app-architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/app-architecture.md b/docs/concepts/app-architecture.md index 88933ac3..6bbb30be 100644 --- a/docs/concepts/app-architecture.md +++ b/docs/concepts/app-architecture.md @@ -98,10 +98,10 @@ Each user gets their own canister, giving them direct control over their data. A ## Data storage -Canisters store data in [stable memory](../guides/backends/stable-memory.md) — there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory: +Canisters store data in [stable memory](../guides/backends/data-persistence.md) — there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory: - **Motoko:** the [`core` standard library](https://mops.one/core/docs) includes persistent data structures that survive upgrades automatically. -- **Rust:** [`ic-stable-structures`](https://docs.rs/ic-cdk/latest/ic_cdk/) provides `StableBTreeMap` and other structures for stable memory. +- **Rust:** [`ic-stable-structures`](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/) provides `StableBTreeMap` and other structures for stable memory. For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the [canister-per-service](#canister-per-service-default) or [canister-per-subnet](#canister-per-subnet) patterns to distribute storage across canisters. From b9ec58831f200bb0e2765d954f5573d545172bd1 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 13 Mar 2026 17:32:22 +0100 Subject: [PATCH 3/3] docs: address review feedback on app architecture page - Distinguish official CDKs from community-supported languages - Fix asset canister not being system-provided - Replace @dfinity/agent with @icp-sdk/core/agent - Fix stable memory wording (heap vs stable distinction) - Fix Motoko persistence wording (not automatic) - Fix onchain/offchain spelling (no hyphens) - Remove canister-per-user pattern - Remove 'default' label from canister-per-service - Reframe single canister as production-viable - Fix 'network compiles' to 'compiled locally' - Soften finality claim with 'typically' --- docs/concepts/app-architecture.md | 43 ++++++++++++------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/concepts/app-architecture.md b/docs/concepts/app-architecture.md index 6bbb30be..edf0db63 100644 --- a/docs/concepts/app-architecture.md +++ b/docs/concepts/app-architecture.md @@ -12,14 +12,14 @@ An application on the Internet Computer typically consists of one or more [canis Most ICP applications start with two canisters: -- **Backend canister** — contains your application logic and data. You write it in Motoko, Rust, TypeScript, or Python, and the network compiles and runs it as WebAssembly. -- **Frontend (asset) canister** — serves your web UI. It is a special system-provided canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP. +- **Backend canister** — contains your application logic and data. You write it in Motoko or Rust (the official CDKs). Community-supported languages like TypeScript and Python are also available — see [Languages](../languages/index.md). Your code is compiled locally to WebAssembly and executed by the network. +- **Frontend (asset) canister** — serves your web UI. It is a standard canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP. When a user opens your application in a browser: 1. The browser sends an HTTPS request to a [boundary node](network-overview.md). 2. The boundary node routes the request to the frontend canister, which returns the HTML and JavaScript. -3. The JavaScript uses an agent library (like `@dfinity/agent`) to send messages to the backend canister. +3. The JavaScript uses an [agent library](https://js.icp.build) (like `@icp-sdk/core/agent`) to send messages to the backend canister. 4. The backend canister processes the message, updates its state if needed, and returns a response. 5. The frontend renders the result. @@ -47,15 +47,15 @@ If you have built on Ethereum or other EVM chains, here is how ICP concepts map: |----------|-----|----------------| | Smart contract | [Canister](canisters.md) | Canisters hold GiBs of state, serve HTTP, run Wasm | | EVM bytecode | WebAssembly | Wasm runs general-purpose code at near-native speed | -| Solidity / Vyper | Motoko, Rust, TypeScript, Python | Multiple language options, full standard libraries | -| Block time (~12s) | Finality (~1–2s) | Update calls finalize in 1–2 seconds | +| Solidity / Vyper | Motoko, Rust (official); TypeScript, Python (community) | Multiple language options, full standard libraries | +| Block time (~12s) | Finality (~1–2s) | Update calls typically finalize in 1–2 seconds | | Gas (user pays) | [Cycles](reverse-gas-model.md) (canister pays) | Users interact for free; developers fund computation | | No HTTP serving | Built-in HTTP serving | Canisters serve web pages directly | -| Off-chain storage (IPFS, etc.) | On-chain stable memory | Up to 500 GiB per canister, no external storage needed | +| Offchain storage (IPFS, etc.) | Onchain stable memory | Up to 500 GiB per canister, no external storage needed | | Bridges / oracles | [Chain-key signing](chain-fusion.md) | Canisters sign transactions on other chains natively | | Immutable by default | Upgradeable by default | Canisters can be upgraded while preserving state | -The biggest shift: on Ethereum, smart contracts are minimal programs that rely on off-chain infrastructure for anything beyond basic state transitions. On ICP, a canister can be an entire application — frontend, backend, database, and scheduled jobs — all on-chain. +The biggest shift: on Ethereum, smart contracts are minimal programs that rely on offchain infrastructure for anything beyond basic state transitions. On ICP, a canister can be an entire application — frontend, backend, database, and scheduled jobs — all onchain. ## Architectural patterns @@ -65,13 +65,13 @@ As your application grows, you can choose from several patterns. Start simple an Everything — assets, logic, and data — lives in one canister. This is the simplest architecture and works well for applications serving up to thousands of users. -**When to use:** prototypes, personal tools, simple dapps, or applications where data and compute fit comfortably within a single canister's limits. +**When to use:** recommended for most applications. A single canister provides atomic operations and minimal maintenance overhead (no cycle management across canisters, no inter-canister call complexity). Consider multi-canister only when you need separation of concerns or hit a single canister's platform limits. -### Canister-per-service (default) +### Canister-per-service -Separate canisters handle distinct responsibilities. The default two-canister setup (frontend + backend) is the simplest form. You can add more canisters as responsibilities grow: one for user data, one for content, one for payments. +Separate canisters handle distinct responsibilities. The two-canister setup (frontend + backend) is the simplest form. You can add more canisters as responsibilities grow: one for user data, one for content, one for payments. -**When to use:** most production applications. This is the recommended starting point. +**When to use:** when you need separation of concerns between components or hit a single canister's platform limits (memory, compute, or storage). **Things to know:** - Inter-canister calls are asynchronous. Code before and after an `await` executes in separate message rounds — this affects atomicity. @@ -88,22 +88,14 @@ For maximum throughput, distribute canisters across multiple [subnets](network-o **Trade-offs:** cross-subnet calls have higher latency and bandwidth limits. You need to design data partitioning carefully. -### Canister-per-user - -Each user gets their own canister, giving them direct control over their data. A factory canister creates and manages individual user canisters. - -**When to use:** applications where users need full sovereignty over their data and compute. This is the most complex pattern and is still an emerging area. - -**Trade-offs:** managing thousands of canisters requires careful cycle management and upgrade coordination. The factory canister must provision enough cycles for each new canister. - ## Data storage -Canisters store data in [stable memory](../guides/backends/data-persistence.md) — there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory: +Canisters store data in heap memory during execution and can persist data across upgrades using [stable memory](../guides/backends/data-persistence.md) — there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory: -- **Motoko:** the [`core` standard library](https://mops.one/core/docs) includes persistent data structures that survive upgrades automatically. +- **Motoko:** the [`core` standard library](https://mops.one/core/docs) includes persistent data structures designed for upgrade-safe storage. - **Rust:** [`ic-stable-structures`](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/) provides `StableBTreeMap` and other structures for stable memory. -For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the [canister-per-service](#canister-per-service-default) or [canister-per-subnet](#canister-per-subnet) patterns to distribute storage across canisters. +For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the [canister-per-service](#canister-per-service) or [canister-per-subnet](#canister-per-subnet) patterns to distribute storage across canisters. ## Frontend options @@ -111,18 +103,17 @@ Not every ICP application needs the default asset canister. Your options: - **Asset canister** — the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to an asset canister that serves it over HTTP. See [Asset canister](../guides/frontends/asset-canister.md). - **Framework-specific canister** — use a framework like Juno that provides a more opinionated hosting solution on ICP. -- **Off-chain frontend** — host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using `@dfinity/agent`. Useful during migration or when you need features that asset canisters don't support. +- **Offchain frontend** — host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using [`@icp-sdk/core/agent`](https://js.icp.build). Useful during migration or when you need features that asset canisters don't support. - **No frontend** — backend-only canisters that expose a Candid API for other canisters or CLI tools to call. ## Choosing an architecture | Question | If yes | If no | |----------|--------|-------| -| Is this a prototype or simple tool? | [Single canister](#single-canister) | Keep reading | +| Start here | [Single canister](#single-canister) — recommended for most applications | — | | Does the app have a web UI? | Add an [asset canister](#the-default-two-canister-model) | Backend-only canister | -| Do you need to separate concerns (auth, storage, compute)? | [Canister-per-service](#canister-per-service-default) | Stick with two canisters | +| Do you need separation of concerns or hit platform limits? | [Canister-per-service](#canister-per-service) | Stay with a single canister | | Do you need to scale beyond one subnet? | [Canister-per-subnet](#canister-per-subnet) | Stay on one subnet | -| Do users need sovereignty over their data? | [Canister-per-user](#canister-per-user) | Use shared canisters | Start with the simplest architecture that meets your requirements. You can always split a canister into multiple canisters later — it is much harder to merge canisters that were split prematurely.