Skip to content
Algorand Developer Portal

App Deployer

← Back to Algorand Client

This example demonstrates the AppDeployer functionality for idempotent application deployment with create, update, and replace strategies:

  • algorand.appDeployer.deploy() for initial deployment
  • Deploy parameters: name, version, approvalProgram, clearProgram, schema, onUpdate, onSchemaBreak
  • Idempotency: calling deploy() again with same version does nothing
  • onUpdate: ‘update’ to update existing app when version changes
  • onUpdate: ‘replace’ to delete and recreate app when version changes
  • onUpdate: ‘fail’ to fail if app already exists with different code
  • onSchemaBreak: ‘replace’ when schema changes require new app
  • Deployment metadata stored in app global state
  • App name used for idempotent lookups
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algorand_client/13-app-deployer.ts

View source on GitHub

13-app-deployer.ts
/**
* Example: App Deployer
*
* This example demonstrates the AppDeployer functionality for idempotent
* application deployment with create, update, and replace strategies:
* - algorand.appDeployer.deploy() for initial deployment
* - Deploy parameters: name, version, approvalProgram, clearProgram, schema, onUpdate, onSchemaBreak
* - Idempotency: calling deploy() again with same version does nothing
* - onUpdate: 'update' to update existing app when version changes
* - onUpdate: 'replace' to delete and recreate app when version changes
* - onUpdate: 'fail' to fail if app already exists with different code
* - onSchemaBreak: 'replace' when schema changes require new app
* - Deployment metadata stored in app global state
* - App name used for idempotent lookups
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { AlgorandClient, algo } from '@algorandfoundation/algokit-utils';
import {
loadTealSource,
printError,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
} from '../shared/utils.js';
// ============================================================================
// TEAL Programs - Versioned Application (loaded from shared artifacts)
// ============================================================================
/**
* Generate a versioned approval program that supports updates and deletes.
* Uses TMPL_UPDATABLE and TMPL_DELETABLE for deploy-time control.
* The version parameter changes the bytecode to simulate code updates.
*/
function getVersionedApprovalProgram(version: number): string {
return loadTealSource('teal-template-versioned.teal').replace(/TMPL_VERSION/g, String(version));
}
// Clear state program (always approves)
const CLEAR_STATE_PROGRAM = loadTealSource('clear-state-approve.teal');
async function main() {
printHeader('App Deployer Example');
// Initialize client and verify LocalNet is running
const algorand = AlgorandClient.defaultLocalNet();
try {
await algorand.client.algod.status();
printSuccess('Connected to LocalNet');
} catch (error) {
printError(
`Failed to connect to LocalNet: ${error instanceof Error ? error.message : String(error)}`,
);
printInfo('Make sure LocalNet is running (e.g., algokit localnet start)');
return;
}
// Step 1: Create and fund test accounts
printStep(1, 'Create and fund test accounts');
printInfo('Creating account for app deployment demonstrations');
const deployer = algorand.account.random();
printInfo(`\nCreated account:`);
printInfo(` Deployer: ${shortenAddress(deployer.addr.toString())}`);
// Fund account generously for multiple deployments
await algorand.account.ensureFundedFromEnvironment(deployer.addr, algo(50));
printSuccess('Created and funded test account');
// Step 2: Initial deployment with appDeployer.deploy()
printStep(2, 'Initial deployment with algorand.appDeployer.deploy()');
printInfo('Deploying a versioned application for the first time');
const appName = 'MyVersionedApp';
const result1 = await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '1.0.0',
updatable: true, // Allow updates via TMPL_UPDATABLE
deletable: true, // Allow deletion via TMPL_DELETABLE
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(1),
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2, // version, counter
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
});
printInfo(`\nDeployment result:`);
printInfo(` Operation performed: ${result1.operationPerformed}`);
printInfo(` App ID: ${result1.appId}`);
printInfo(` App Address: ${shortenAddress(result1.appAddress.toString())}`);
printInfo(` App Name: ${result1.name}`);
printInfo(` Version: ${result1.version}`);
printInfo(` Updatable: ${result1.updatable}`);
printInfo(` Deletable: ${result1.deletable}`);
if ('transaction' in result1) {
printInfo(` Transaction ID: ${result1.txIds[0]}`);
}
printSuccess('Initial deployment completed (operation: create)');
// Step 3: Demonstrate idempotency - same version does nothing
printStep(3, 'Demonstrate idempotency - deploy same version again');
printInfo('Calling deploy() again with the same version should do nothing');
const result2 = await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '1.0.0', // Same version
updatable: true,
deletable: true,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(1), // Same code
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2,
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
});
printInfo(`\nIdempotent deployment result:`);
printInfo(` Operation performed: ${result2.operationPerformed}`);
printInfo(` App ID: ${result2.appId} (same as before)`);
printInfo(` Version: ${result2.version}`);
if (result2.operationPerformed === 'nothing') {
printInfo(` Note: No transaction was sent - app is unchanged`);
}
printSuccess('Idempotency verified - no action taken for same version');
// Step 4: Demonstrate onUpdate: 'update'
printStep(4, "Demonstrate onUpdate: 'update' - update existing app");
printInfo('Deploying version 2.0.0 with onUpdate: "update" to update the existing app');
const result3 = await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '2.0.0', // New version
updatable: true,
deletable: true,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(2), // Updated code
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2,
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
onUpdate: 'update', // Update the existing app
});
printInfo(`\nUpdate deployment result:`);
printInfo(` Operation performed: ${result3.operationPerformed}`);
printInfo(` App ID: ${result3.appId} (same app, updated in place)`);
printInfo(` Version: ${result3.version}`);
printInfo(` Created round: ${result3.createdRound}`);
printInfo(` Updated round: ${result3.updatedRound}`);
if ('transaction' in result3) {
printInfo(` Transaction ID: ${result3.txIds[0]}`);
}
// Verify the global state was preserved but version updated
const globalState = await algorand.app.getGlobalState(result3.appId);
printInfo(`\nGlobal state after update:`);
printInfo(` version: ${globalState['version']?.value ?? 'N/A'} (from TEAL)`);
printInfo(` counter: ${globalState['counter']?.value ?? 'N/A'} (preserved)`);
printSuccess('App updated in place with new code');
// Step 5: Demonstrate onUpdate: 'fail'
printStep(5, "Demonstrate onUpdate: 'fail' - fails if update detected");
printInfo('Trying to deploy version 3.0.0 with onUpdate: "fail" should throw an error');
try {
await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '3.0.0', // New version
updatable: true,
deletable: true,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(3),
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2,
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
onUpdate: 'fail', // Fail if update detected
});
printError('Expected an error but deployment succeeded');
} catch (error) {
printInfo(`\nExpected error caught:`);
printInfo(` ${error instanceof Error ? error.message : String(error)}`);
printSuccess('onUpdate: "fail" correctly prevents updates');
}
// Step 6: Demonstrate onUpdate: 'replace'
printStep(6, "Demonstrate onUpdate: 'replace' - delete and recreate app");
printInfo('Deploying version 3.0.0 with onUpdate: "replace" deletes old app and creates new one');
const oldAppId = result3.appId;
const result4 = await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '3.0.0',
updatable: true,
deletable: true,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(3),
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2,
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
onUpdate: 'replace', // Delete old and create new
});
printInfo(`\nReplace deployment result:`);
printInfo(` Operation performed: ${result4.operationPerformed}`);
printInfo(` Old App ID: ${oldAppId} (deleted)`);
printInfo(` New App ID: ${result4.appId}`);
printInfo(` App Address: ${shortenAddress(result4.appAddress.toString())}`);
printInfo(` Version: ${result4.version}`);
if (result4.operationPerformed === 'replace' && 'deleteResult' in result4) {
printInfo(
` Delete transaction confirmed: round ${result4.deleteResult.confirmation.confirmedRound}`,
);
}
printSuccess('Old app deleted and new app created');
// Step 7: Demonstrate onSchemaBreak: 'replace'
printStep(7, "Demonstrate onSchemaBreak: 'replace' - handle schema changes");
printInfo('Deploying version 4.0.0 with increased schema (more global ints)');
printInfo('Schema changes cannot be done via update, so replace is required');
const result5 = await algorand.appDeployer.deploy({
metadata: {
name: appName,
version: '4.0.0',
updatable: true,
deletable: true,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(4),
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 3, // Schema break: increased from 2 to 3
globalByteSlices: 1, // Schema break: increased from 0 to 1
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
onUpdate: 'update', // Would normally try to update
onSchemaBreak: 'replace', // But schema change forces replace
});
printInfo(`\nSchema break deployment result:`);
printInfo(` Operation performed: ${result5.operationPerformed}`);
printInfo(` Previous App ID: ${result4.appId}`);
printInfo(` New App ID: ${result5.appId}`);
printInfo(` Version: ${result5.version}`);
// Verify new schema
const appInfo = await algorand.app.getById(result5.appId);
printInfo(`\nNew app schema:`);
printInfo(` globalInts: ${appInfo.globalInts}`);
printInfo(` globalByteSlices: ${appInfo.globalByteSlices}`);
printSuccess('Schema break handled with replace strategy');
// Step 8: Show deployment metadata in global state
printStep(8, 'Show deployment metadata lookup by name');
printInfo('The appDeployer uses app name for idempotent lookups across deployments');
// Look up the app by creator
const creatorApps = await algorand.appDeployer.getCreatorAppsByName(deployer.addr);
printInfo(`\nApps deployed by ${shortenAddress(deployer.addr.toString())}:`);
for (const [name, appMeta] of Object.entries(creatorApps.apps)) {
printInfo(`\n App Name: "${name}"`);
printInfo(` App ID: ${appMeta.appId}`);
printInfo(` Version: ${appMeta.version}`);
printInfo(` Updatable: ${appMeta.updatable}`);
printInfo(` Deletable: ${appMeta.deletable}`);
printInfo(` Created Round: ${appMeta.createdRound}`);
printInfo(` Updated Round: ${appMeta.updatedRound}`);
printInfo(` Deleted: ${appMeta.deleted}`);
printInfo(` Created Metadata:`);
printInfo(` Name: ${appMeta.createdMetadata.name}`);
printInfo(` Version: ${appMeta.createdMetadata.version}`);
}
printSuccess('Deployment metadata retrieved');
// Step 9: Demonstrate how name enables idempotency
printStep(9, 'Demonstrate how app name enables idempotent deployments');
printInfo('Deploy a second app with a different name to show name-based lookup');
const result6 = await algorand.appDeployer.deploy({
metadata: {
name: 'AnotherApp', // Different name
version: '1.0.0',
updatable: false,
deletable: false,
},
createParams: {
sender: deployer.addr,
approvalProgram: getVersionedApprovalProgram(100),
clearStateProgram: CLEAR_STATE_PROGRAM,
schema: {
globalInts: 2, // version, counter
globalByteSlices: 0,
localInts: 0,
localByteSlices: 0,
},
},
updateParams: {
sender: deployer.addr,
},
deleteParams: {
sender: deployer.addr,
},
});
printInfo(`\nSecond app deployment:`);
printInfo(` Name: AnotherApp`);
printInfo(` App ID: ${result6.appId}`);
printInfo(` Operation: ${result6.operationPerformed}`);
// Now list all apps again
const allApps = await algorand.appDeployer.getCreatorAppsByName(deployer.addr);
printInfo(`\nAll apps by creator (${Object.keys(allApps.apps).length} apps):`);
for (const name of Object.keys(allApps.apps)) {
printInfo(` - "${name}" (App ID: ${allApps.apps[name].appId})`);
}
printSuccess('Multiple apps tracked by name');
// Step 10: Summary
printStep(10, 'Summary - App Deployer API');
printInfo('The AppDeployer provides idempotent application deployment:');
printInfo('');
printInfo('algorand.appDeployer.deploy(params):');
printInfo(' - Deploys applications with idempotent behavior based on app name');
printInfo(' - Returns: AppDeployResult with operationPerformed discriminator');
printInfo('');
printInfo('Key parameters:');
printInfo(' metadata: { name, version, updatable, deletable }');
printInfo(' - name: Unique identifier for idempotent lookups');
printInfo(' - version: Semantic version string');
printInfo(' - updatable/deletable: Deploy-time controls (TMPL_UPDATABLE/TMPL_DELETABLE)');
printInfo('');
printInfo(' createParams: { sender, approvalProgram, clearStateProgram, schema }');
printInfo(' updateParams: { sender } - Used for update operations');
printInfo(' deleteParams: { sender } - Used for replace operations');
printInfo('');
printInfo(' onUpdate: Controls behavior when code changes:');
printInfo(" 'fail' - Throw error (default)");
printInfo(" 'update' - Update app in place (preserves app ID)");
printInfo(" 'replace' - Delete old app, create new one");
printInfo(" 'append' - Create new app, leave old one");
printInfo('');
printInfo(' onSchemaBreak: Controls behavior when schema changes:');
printInfo(" 'fail' - Throw error (default)");
printInfo(" 'replace' - Delete old app, create new one");
printInfo(" 'append' - Create new app, leave old one");
printInfo('');
printInfo('operationPerformed values:');
printInfo(" 'create' - New app was created");
printInfo(" 'update' - Existing app was updated in place");
printInfo(" 'replace' - Old app deleted, new app created");
printInfo(" 'nothing' - No changes (idempotent)");
printInfo('');
printInfo('algorand.appDeployer.getCreatorAppsByName(creator):');
printInfo(' - Lists all apps deployed by a creator with their metadata');
printInfo(' - Used internally for idempotent lookup by name');
printSuccess('App Deployer example completed!');
}
main().catch(error => {
printError(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});