Skip to main content
On-chain KMS uses a smart contract on Ethereum or Base as the key management layer. Only the contract owner can authorize compose file changes — enabling multisig, timelock, or DAO governance. For a conceptual overview, see Cloud vs On-Chain KMS.

PHALA vs On-Chain KMS

FeaturePHALA KMSOn-Chain KMS
Key managementCentralized Phala serviceSmart contract on-chain
Compose updatesDirect API callTwo-phase: register on-chain, then API
CostFreeGas fees
ChainNoneEthereum or Base

Deploy with CLI

The CLI handles the full on-chain flow automatically when you provide a private key:
phala deploy \
  -c docker-compose.yml \
  -e .env \
  --kms base \
  --private-key <key> \
  --rpc-url <url>
This provisions the CVM, deploys the DstackApp contract, registers the compose hash and device on-chain, and starts the CVM.

Update with CLI

Single Owner

If you hold the contract owner’s private key:
phala deploy --cvm-id app_abc123 \
  -c docker-compose.yml \
  --private-key <key> \
  --rpc-url <url>
The CLI detects the compose hash change, registers it on-chain, and completes the update.

Multisig Owner (Two-Phase)

When the contract owner is a multisig wallet (e.g., Gnosis Safe), the on-chain transaction requires multiple signatures. The CLI supports a prepare → approve → commit flow: Phase 1 — Prepare:
phala deploy --cvm-id app_abc123 \
  --prepare-only \
  -c docker-compose.yml \
  -e .env
Output:
Compose Hash:    0xff22c67f...
App ID:          09cef33ca9...
Chain:           Base (ID: 8453)
Contract:        https://basescan.org/address/0x09cef33c...
Commit Token:    a5732e36-...
Commit URL:      https://cloud.phala.com/my-team/cvms/.../confirm-update?token=...
API Commit URL:  https://cloud-api.phala.com/api/v1/cvms/.../commit-update?token=... (POST)

On-chain Status:
  Compose Hash:  NOT registered
  Device ID:     registered
Multisig Approval: Use the compose hash to propose addComposeHash(bytes32) on the DstackApp contract through your multisig wallet. Phase 2 — Commit:
phala deploy --cvm-id app_abc123 \
  --commit \
  --token <commit-token>
Or commit via the API directly:
curl -X POST 'https://cloud-api.phala.com/api/v1/cvms/<uuid>/commit-update?token=<token>'
Or use the Confirm Update page in the dashboard (link in --prepare-only output).
The commit token is valid for 7 days. A new --prepare-only request invalidates any previous token for the same CVM.

Webhook Notifications

Configure webhooks to receive cvm.update.pending_approval events when a prepare-only update is created. The payload includes the compose hash, commit token, and commit URL for CI/CD or notification integration.

Deploy with SDK

For programmatic deployments, use the @phala/cloud SDK:

Step 1: Provision

import { createClient } from "@phala/cloud";

const client = createClient();

const provision = await client.provisionCvm({
  name: "my-app",
  compose_file: {
    docker_compose_file: composeYaml,
    allowed_envs: ["API_KEY"],
  },
  kms: "BASE",
});

Step 2: Deploy Contract

import { deployAppAuth } from "@phala/cloud";

const deployed = await deployAppAuth({
  chain: provision.kms_info.chain,
  rpcUrl: "https://...",
  kmsContractAddress: provision.kms_info.kms_contract_address,
  privateKey: "0x...",
  deviceId: provision.device_id,
  composeHash: provision.compose_hash,
});

Step 3: Commit

import { encryptEnvVars, parseEnvVars } from "@phala/cloud";

const { public_key } = await client.getAppEnvEncryptPubKey({
  kms: provision.kms_info.slug,
  app_id: deployed.appId,
});

const encrypted = await encryptEnvVars(parseEnvVars("API_KEY=secret"), public_key);

await client.commitCvmProvision({
  app_id: deployed.appId,
  compose_hash: provision.compose_hash,
  encrypted_env: encrypted,
  kms_id: provision.kms_info.slug,
  contract_address: deployed.appAuthAddress,
  deployer_address: deployed.deployer,
});

Update with SDK

Two-Phase Update

import { patchCvm, confirmCvmPatch, addComposeHash } from "@phala/cloud";

// Phase 1
const result = await patchCvm(client, {
  id: "my-app",
  docker_compose_file: newYaml,
});

if (result.requiresOnChainHash) {
  // Register on-chain
  const tx = await addComposeHash({
    chain: result.kmsInfo.chain,
    appId: result.appId as `0x${string}`,
    composeHash: result.composeHash,
    privateKey: "0x...",
  });

  // Phase 2
  await confirmCvmPatch(client, {
    id: "my-app",
    composeHash: result.composeHash,
    transactionHash: tx.transactionHash,
  });
}

Prepare-Only (for Multisig)

const result = await patchCvm(client, {
  id: "my-app",
  docker_compose_file: newYaml,
  prepareOnly: true,
});

if (result.requiresOnChainHash) {
  console.log("Compose hash:", result.composeHash);
  console.log("Commit token:", result.commitToken);
  console.log("On-chain status:", result.onchainStatus);
  // → Hand off to multisig for approval
}

Device Management

On-chain KMS uses a device allowlist in the smart contract. The CLI handles device registration automatically. For programmatic control:
import { addDevice, removeDevice, setAllowAnyDevice, checkDeviceAllowed } from "@phala/cloud";

// Add a device
await addDevice({ chain: base, appAddress: "0x...", deviceId: "0x...", privateKey: "0x..." });

// Check if allowed
const allowed = await checkDeviceAllowed({ chain: base, appAddress: "0x...", deviceId: "0x..." });

// Allow any device (bypass allowlist)
await setAllowAnyDevice({ chain: base, appAddress: "0x...", allow: true, privateKey: "0x..." });

checkOnChainPrerequisites

Batch-check device and compose hash registration in a single RPC call:
import { checkOnChainPrerequisites } from "@phala/cloud";

const prereqs = await checkOnChainPrerequisites({
  chain: base,
  appAddress: "0x...",
  deviceId: "0x...",
  composeHash: "0x...",
});
// prereqs.deviceAllowed, prereqs.composeHashAllowed