diff --git a/docs/guides/authentication/internet-identity.md b/docs/guides/authentication/internet-identity.md
deleted file mode 100644
index 87143994..00000000
--- a/docs/guides/authentication/internet-identity.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: "Internet Identity"
-description: "Integrate passkey-based authentication with Internet Identity"
-sidebar:
- order: 1
----
-
-TODO: Write content for this page.
-
-
-Integrate Internet Identity (II) for passkey-based user authentication. Cover frontend setup with @icp-sdk/auth (AuthClient), delegation handling, principal-per-app isolation, and alternative origins configuration. Include Unity native app integration via deep links. Explain the delegation chain and session management.
-
-
-- Portal: building-apps/authentication/overview.mdx, integrate-internet-identity.mdx, alternative-origins.mdx
-- icskills: internet-identity
-- JS SDK: @icp-sdk/auth (https://js.icp.build/auth)
-- Examples: internet_identity_integration (Motoko), encrypted-notes-dapp-vetkd (both), native-apps/unity_ii_* (3 variants)
-
-
-- concepts/security -- identity and trust
-- guides/frontends/frameworks -- framework-specific auth setup
-- guides/defi/wallet-integration -- alternative to II
-- reference/internet-identity-spec -- protocol details
diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx
new file mode 100644
index 00000000..b492d54d
--- /dev/null
+++ b/docs/guides/authentication/internet-identity.mdx
@@ -0,0 +1,328 @@
+---
+title: "Internet Identity"
+description: "Integrate passkey-based authentication with Internet Identity for frontend login, backend caller verification, and session management"
+sidebar:
+ order: 1
+---
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+
+Internet Identity (II) is the Internet Computer's native authentication system. Users sign in with passkeys or OpenID accounts (Google, Apple, Microsoft) instead of passwords. Each user receives a unique principal per frontend origin, preventing cross-app tracking.
+
+This guide covers setting up II authentication end-to-end: configuring your project, adding login to your frontend, and verifying callers in your backend.
+
+## How it works
+
+When a user authenticates through Internet Identity, the following happens:
+
+1. Your frontend opens an II popup window.
+2. The user authenticates with a passkey or OpenID provider.
+3. II creates a **delegation identity** — a temporary key pair that can sign messages on behalf of the user's master key.
+4. Your frontend receives this delegation and uses it to sign canister calls.
+5. The backend canister sees the user's **principal** (derived from the delegation chain) as `msg.caller`.
+
+**Principal-per-app isolation:** II derives a different principal for each frontend origin. A user logging into `https://app-a.icp0.io` gets a different principal than when logging into `https://app-b.icp0.io`, even with the same passkey. This prevents apps from correlating users across services.
+
+**Delegations expire.** The frontend sets a `maxTimeToLive` when requesting the delegation (default recommendation: 8 hours). After expiry, the user must re-authenticate. The maximum allowed delegation lifetime is 30 days (2,592,000,000,000,000 nanoseconds).
+
+## Project setup
+
+### Configure icp.yaml for local Internet Identity
+
+Add `ii: true` to your local network configuration. This tells icp-cli to deploy a local Internet Identity canister automatically:
+
+```yaml
+networks:
+ - name: local
+ mode: managed
+ ii: true
+```
+
+### Install frontend packages
+
+```bash
+npm install @icp-sdk/auth @icp-sdk/core
+```
+
+## Frontend integration
+
+The `AuthClient` from `@icp-sdk/auth` handles the full login flow: opening the II popup, receiving the delegation, and managing session persistence.
+
+### Environment detection
+
+Internet Identity runs at different URLs in local development versus mainnet. II uses a well-known frontend canister (`uqzsh-gqaaa-aaaaq-qaada-cai`) that you authenticate against. Detect the host to return the right URL:
+
+```javascript
+import { AuthClient } from "@icp-sdk/auth/client";
+import { HttpAgent, Actor } from "@icp-sdk/core/agent";
+import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
+
+// Read the ic_env cookie set by the asset canister or Vite dev server.
+// Contains IC_ROOT_KEY and canister IDs — works in both local and production without
+// environment branching. Available in browser contexts only; see note below for Node.js.
+const canisterEnv = safeGetCanisterEnv();
+
+function getIdentityProviderUrl() {
+ const host = window.location.hostname;
+ const isLocal =
+ host === "localhost" ||
+ host === "127.0.0.1" ||
+ host.endsWith(".localhost");
+
+ if (isLocal) {
+ // icp-cli sets up a local alias: http://id.ai.localhost:8000
+ return "http://id.ai.localhost:8000";
+ }
+ return "https://id.ai";
+}
+```
+
+### Login, logout, and session check
+
+Create a single `AuthClient` instance on page load and reuse it for all operations:
+
+```javascript
+// Create the auth client (once, on page load)
+const authClient = await AuthClient.create();
+
+// Check for existing session
+const isAuthenticated = await authClient.isAuthenticated();
+if (isAuthenticated) {
+ const identity = authClient.getIdentity();
+ // Restore session — create agent and actor with this identity
+}
+
+// Login
+async function login() {
+ return new Promise((resolve, reject) => {
+ authClient.login({
+ identityProvider: getIdentityProviderUrl(),
+ maxTimeToLive: BigInt(8) * BigInt(3_600_000_000_000), // 8 hours
+ onSuccess: () => {
+ const identity = authClient.getIdentity();
+ console.log("Logged in as:", identity.getPrincipal().toText());
+ resolve(identity);
+ },
+ onError: (error) => {
+ console.error("Login failed:", error);
+ reject(error);
+ },
+ });
+ });
+}
+
+// Logout
+async function logout() {
+ await authClient.logout();
+ // Reset UI state or reload
+}
+```
+
+### Create an authenticated agent
+
+After login, create an `HttpAgent` using the delegation identity. The agent signs all subsequent canister calls with the user's delegated key:
+
+```javascript
+async function createAuthenticatedActor(identity, canisterId, idlFactory) {
+ const agent = await HttpAgent.create({
+ identity,
+ host: window.location.origin,
+ rootKey: canisterEnv?.IC_ROOT_KEY,
+ });
+
+ return Actor.createActor(idlFactory, { agent, canisterId });
+}
+```
+
+:::note[Node.js environments]
+`safeGetCanisterEnv()` reads the `ic_env` cookie set by the asset canister or Vite dev server — it only works in browser contexts. For Node.js scripts or tests connecting to a **local** replica, create the agent normally and call `await agent.fetchRootKey()` explicitly after creation. Never call `fetchRootKey()` against a mainnet endpoint — on mainnet the root key is pre-trusted, and fetching it at runtime exposes a man-in-the-middle risk.
+:::
+
+## Backend authentication
+
+Your backend canister receives the caller's principal automatically through the IC protocol. You do not pass the principal as a function argument — use `msg.caller` (Motoko) or `ic_cdk::api::msg_caller()` (Rust) to read it.
+
+### Reject anonymous callers
+
+Any unauthenticated request uses the anonymous principal (`2vxsx-fae`). Reject it in protected endpoints:
+
+
+
+
+```motoko
+import Principal "mo:core/Principal";
+import Runtime "mo:core/Runtime";
+
+persistent actor {
+ func requireAuth(caller : Principal) : () {
+ if (Principal.isAnonymous(caller)) {
+ Runtime.trap("Anonymous principal not allowed.");
+ };
+ };
+
+ public shared query ({ caller }) func whoAmI() : async Text {
+ if (Principal.isAnonymous(caller)) {
+ "anonymous"
+ } else {
+ Principal.toText(caller)
+ };
+ };
+
+ public shared ({ caller }) func protectedAction() : async Text {
+ requireAuth(caller);
+ "Action performed by " # Principal.toText(caller)
+ };
+};
+```
+
+
+
+
+```rust
+use candid::Principal;
+use ic_cdk::{query, update};
+
+fn require_auth() -> Principal {
+ let caller = ic_cdk::api::msg_caller();
+ if caller == Principal::anonymous() {
+ ic_cdk::trap("Anonymous principal not allowed.");
+ }
+ caller
+}
+
+#[query]
+fn who_am_i() -> String {
+ let caller = ic_cdk::api::msg_caller();
+ if caller == Principal::anonymous() {
+ "anonymous".to_string()
+ } else {
+ format!("{}", caller)
+ }
+}
+
+#[update]
+fn protected_action() -> String {
+ let caller = require_auth();
+ format!("Action performed by {}", caller)
+}
+```
+
+
+
+
+### Rust: capture caller before await
+
+In async update functions, bind the caller at the top of the function before any `.await` points. The current ic-cdk executor preserves the caller across await points, but capturing it early is a defensive practice that guards against future executor changes:
+
+```rust
+#[update]
+async fn protected_async_action() -> String {
+ let caller = require_auth(); // Capture before any await
+ // Replace with your actual async canister call, e.g.:
+ // ic_cdk::call::<_, (String,)>(some_canister_id, "some_method", ()).await
+ format!("Action completed by {}", caller)
+}
+```
+
+## Local development
+
+Start the local network and deploy. With `ii: true` in your `icp.yaml`, icp-cli deploys a local Internet Identity canister automatically:
+
+```bash
+icp network start
+icp deploy
+```
+
+icp-cli pulls the mainnet II Wasm when deploying locally and registers a local alias so the II frontend is reachable at `http://id.ai.localhost:8000`. Use the `getIdentityProviderUrl` helper (shown in the environment detection section above) to point to this URL in local development.
+
+To test authentication from the command line:
+
+```bash
+# Test as the default identity (authenticated)
+icp canister call backend whoAmI
+
+# Test as anonymous using --identity to avoid changing your global default
+icp canister call backend protectedAction --identity anonymous
+# Expected: Error containing "Anonymous principal not allowed"
+```
+
+For mainnet deployment, Internet Identity is already running — backend canister `rdmx6-jaaaa-aaaaa-aaadq-cai` and frontend canister `uqzsh-gqaaa-aaaaq-qaada-cai` (served at `https://id.ai`). Both IDs are identical on local replicas when `ii: true` is configured. Deploy only your own canisters:
+
+```bash
+icp deploy -e ic
+```
+
+## Alternative origins
+
+By default, each frontend origin produces a different user principal. If you serve your app from multiple domains (for example, migrating from `.icp0.io` to a custom domain), users would get different principals on each domain.
+
+:::note
+II now automatically handles the `icp0.io` vs `ic0.app` domain difference — you do **not** need to use `derivationOrigin` or `ii-alternative-origins` for that case. Use alternative origins only when you have two genuinely distinct custom domains that should share the same user principal.
+:::
+
+To keep principals consistent across your own custom domains, configure **alternative origins**:
+
+1. **On the primary origin (A):** Create a file at `.well-known/ii-alternative-origins` listing the alternative domains:
+
+ ```json
+ {
+ "alternativeOrigins": ["https://www.yourcustomdomain.com"]
+ }
+ ```
+
+ A maximum of 10 alternative origins can be listed. No trailing slashes or paths.
+
+2. **Configure the asset canister** to serve the `.well-known` directory. Add an `.ic-assets.json5` in your frontend source:
+
+ ```json
+ [
+ {
+ "match": ".well-known",
+ "ignore": false
+ },
+ {
+ "match": ".well-known/ii-alternative-origins",
+ "headers": {
+ "Access-Control-Allow-Origin": "*",
+ "Content-Type": "application/json"
+ },
+ "ignore": false
+ }
+ ]
+ ```
+
+3. **On the alternative origin (B):** Set the `derivationOrigin` in your login call to point back to the primary origin:
+
+ ```javascript
+ authClient.login({
+ identityProvider: "https://id.ai",
+ derivationOrigin: "https://xxxxx.icp0.io", // primary origin A
+ onSuccess: () => { /* ... */ },
+ });
+ ```
+
+ The primary origin (A) does not need `derivationOrigin` — it is only required on alternative origins.
+
+For full details, see the [Internet Identity specification](../../reference/internet-identity-spec.md).
+
+## Common mistakes
+
+- **Using the wrong II URL per environment** — local development must point to `http://id.ai.localhost:8000`, mainnet to `https://id.ai`. Use the `getIdentityProviderUrl` helper (shown above) to switch based on hostname.
+- **`fetch` "Illegal invocation" in bundled builds** — always pass `fetch: window.fetch.bind(window)` to `HttpAgent.create()`. Without explicit binding, bundlers (Vite, webpack) extract `fetch` from `window` and call it without the correct `this` context.
+- **Missing `onSuccess`/`onError` callbacks** — `authClient.login()` requires both. Without them, login failures are silently swallowed.
+- **Delegation expiry too long** — the maximum is 30 days. Values above this are silently clamped, causing confusing session behavior. Use 8 hours for typical apps.
+- **Passing principal as a string argument** — the backend reads the caller automatically from the IC protocol. Do not pass it as a function parameter.
+- **Using `shouldFetchRootKey: true` in browser code** — pass `rootKey: canisterEnv?.IC_ROOT_KEY` from `safeGetCanisterEnv()` instead. `shouldFetchRootKey: true` fetches the root key from the replica at runtime, which lets a man-in-the-middle substitute a fake key on mainnet. For Node.js scripts targeting a local replica only, `await agent.fetchRootKey()` is acceptable — but never on mainnet.
+- **Creating multiple `AuthClient` instances** — create one on page load and reuse it. Multiple instances cause race conditions with session storage.
+
+## Next steps
+
+- [Wallet integration](../defi/wallet-integration.md) for token-based authentication alternatives
+- [Frontend frameworks](../frontends/frameworks.md) for framework-specific auth setup patterns
+- [Internet Identity specification](../../reference/internet-identity-spec.md) for protocol details and the full alternative origins spec
+- [Security best practices](../../concepts/security.md) for identity and trust fundamentals
+- [AuthClient API reference](https://js.icp.build) for the full `@icp-sdk/auth` API
+
+{/* TODO: Add Unity native app integration via deep links — see portal native-apps/unity_ii_* */}
+
+{/* Upstream: informed by dfinity/portal — docs/building-apps/authentication/overview.mdx, docs/building-apps/authentication/integrate-internet-identity.mdx, docs/building-apps/authentication/alternative-origins.mdx; dfinity/icskills — skills/internet-identity/SKILL.md */}