Skip to main content
On-chain KMS uses a smart contract on Ethereum or Base as the key management layer instead of the centralized Phala KMS. This provides decentralized access control — only the contract owner can authorize compose file changes and environment variable updates.

PHALA vs On-Chain KMS

FeaturePHALA KMSOn-Chain KMS (ETHEREUM/BASE)
Key managementCentralized Phala serviceSmart contract on-chain
App ID sourceReturned by provisionCvmGenerated by deployAppAuth contract
Compose updatesDirect API callTwo-phase: register on-chain, then API call
Env var updatesDirect API callTwo-phase when env_keys change
CostFreeGas fees for contract interactions
ChainNoneEthereum mainnet or Base

Full Deploy Flow

Step 1: Provision the CVM

import { createClient, encryptEnvVars, parseEnvVars } from "@phala/cloud";
import { deployAppAuth } 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: "ETHEREUM", // or "BASE"
});
With on-chain KMS, provision.app_id is null — you need to deploy a contract to get one.

Step 2: Fetch KMS Details

const kmsList = await client.getKmsList({ is_onchain: true });
const kms = kmsList.items.find(k => k.id === provision.kms_id || k.slug === provision.kms_id);

// kms.chain — viem Chain config
// kms.kms_contract_address — factory contract address

Step 3: Deploy AppAuth Contract

const deployed = await deployAppAuth({
  chain: kms.chain,
  rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/...",
  kmsContractAddress: kms.kms_contract_address,
  privateKey: "0xabc123...",
  deviceId: provision.device_id,
  composeHash: provision.compose_hash,
});

console.log(`App ID: ${deployed.appId}`);
console.log(`Contract: ${deployed.appAuthAddress}`);

Step 4: Encrypt Environment Variables

// Get the encryption public key from KMS
const { public_key } = await client.getAppEnvEncryptPubKey({
  kms: kms.slug,
  app_id: deployed.appId,
});

const envVars = parseEnvVars("API_KEY=my-secret-key");
const encrypted = await encryptEnvVars(envVars, public_key);

Step 5: Commit the Provision

const cvm = await client.commitCvmProvision({
  app_id: deployed.appId,
  compose_hash: provision.compose_hash,
  encrypted_env: encrypted,
  env_keys: ["API_KEY"],
  kms_id: kms.slug,
  contract_address: deployed.appAuthAddress,
  deployer_address: deployed.deployer,
});

console.log("CVM deployed:", cvm.id);

Updating Environment Variables

When you update env vars on an on-chain KMS CVM and the set of env_keys changes, the API returns precondition_required:
import { addComposeHash } from "@phala/cloud";

// Phase 1: attempt the update
const result = await client.updateCvmEnvs({
  id: "my-app",
  encrypted_env: newEncrypted,
  env_keys: ["API_KEY", "NEW_VAR"], // env_keys changed
});

if (result.status === "precondition_required") {
  // Phase 2a: register the new compose hash on-chain
  const receipt = await addComposeHash({
    chain: result.kms_info.chain,
    kmsContractAddress: result.kms_info.kms_contract_address,
    appId: result.app_id as `0x${string}`,
    composeHash: result.compose_hash,
    privateKey: privateKey,
  });

  // Phase 2b: retry with on-chain proof
  await client.updateCvmEnvs({
    id: "my-app",
    encrypted_env: newEncrypted,
    env_keys: ["API_KEY", "NEW_VAR"],
    compose_hash: result.compose_hash,
    transaction_hash: receipt.transactionHash,
  });
}
If the env keys don’t change (same keys, different values), the update goes through in a single call.

Updating Docker Compose

The same two-phase flow applies to Docker Compose updates:
// Phase 1
const result = await client.updateDockerCompose({
  id: "my-app",
  docker_compose_file: newYaml,
});

if (result.status === "precondition_required") {
  // Register on-chain
  const receipt = await addComposeHash({
    chain: result.kms_info.chain,
    kmsContractAddress: result.kms_info.kms_contract_address,
    appId: result.app_id as `0x${string}`,
    composeHash: result.compose_hash,
    privateKey: privateKey,
  });

  // Retry with proof
  await client.updateDockerCompose({
    id: "my-app",
    docker_compose_file: newYaml,
    compose_hash: result.compose_hash,
    transaction_hash: receipt.transactionHash,
  });
}

Updating via Compose File API

For more control, use the two-phase compose file update API:
// Get current compose file
const compose = await client.getCvmComposeFile({ id: "my-app" });
compose.docker_compose_file = newYaml;
compose.allowed_envs = ["API_KEY", "NEW_VAR"];

// Provision the update (get compose_hash)
const provision = await client.provisionCvmComposeFileUpdate({
  uuid: "my-app",
  app_compose: compose,
});

// Register on-chain
const receipt = await addComposeHash({
  chain: cvm.kms_info.chain,
  rpcUrl: rpcUrl,
  appId: cvm.app_id as `0x${string}`,
  composeHash: provision.compose_hash,
  privateKey: privateKey,
});

// Commit the update
await client.commitCvmComposeFileUpdate({
  id: "my-app",
  compose_hash: provision.compose_hash,
  encrypted_env: encrypted,
  env_keys: ["API_KEY", "NEW_VAR"],
});

Listing KMS Instances

const kmsList = await client.getKmsList();
for (const kms of kmsList.items) {
  if (kms.chain_id) {
    console.log(`${kms.slug} (on-chain): chain=${kms.chain_id}, contract=${kms.kms_contract_address}`);
  } else {
    console.log(`${kms.slug} (centralized)`);
  }
}