Edit in GitHubLog an issue

Add-on Architecture Guide

Learn about the dual-runtime architecture, communication patterns, and development workflow essentials for building Adobe Express add-ons.

Overview

Understanding the Adobe Express add-on architecture is crucial for building effective add-ons. This comprehensive deep-dive guide covers the dual-runtime system, cross-runtime communication patterns, SDK imports and usage, debugging techniques, security considerations, performance optimization, and development best practices. Whether you're new to add-on development or looking to master advanced concepts, this guide provides the foundational knowledge needed to build robust, secure, and performant add-ons.

Key Architectural Concept: Add-ons are bundled and served as iframes within Adobe Express, with isolated runtime environments communicating through a secure proxy layer.

Dual-Runtime Architecture

Adobe Express add-ons run in two separate JavaScript execution environments that work together:

  1. iframe runtime - Your add-on's user interface environment (uses Add-on UI SDK)
  2. document sandbox - Secure environment for document manipulation (uses Document Sandbox SDK)

Architectural Implementation

Your add-on is bundled and loaded as an iframe within Adobe Express. Both runtime environments are isolated from each other and from the host application for security. They communicate exclusively through a proxy-based message passing system, which ensures that:

  • The UI cannot directly access or manipulate the document
  • The document sandbox cannot directly access the DOM or browser APIs
  • All cross-runtime communication is type-safe and validated
  • Security boundaries are maintained while enabling powerful functionality

This isolation is fundamental to Adobe Express's security model, allowing third-party add-ons to extend functionality without compromising user data or application stability.

Architecture Diagram

Runtime Architecture Diagram

What is the Runtime Object?

The runtime object provides communication APIs that enable the two environments to talk to each other. Each environment has its own runtime object:

  • iframe runtime: addOnUISdk.instance.runtime (from Add-on UI SDK)
  • document sandbox: addOnSandboxSdk.instance.runtime (from Document Sandbox SDK)

Think of the runtime object like a phone - each side has their own phone with two key methods:

  • exposeApi() - Makes your functions callable from the other side (like publishing your phone number)
  • apiProxy() - Gets a proxy to call functions on the other side (like dialing the other phone)

The Two Environments Explained

iframe Runtime

What it is: The browser environment where your add-on's user interface runs
File: index.js or index.html
SDK Used: Add-on UI SDK
SDK Import:

Copied to your clipboard
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";

Primary Role: Handle user interactions, render UI, access browser APIs, import/export media

Document Sandbox

What it is: The secure isolated environment where document manipulation code runs
File: code.js
SDK Used: Document Sandbox SDK (for communication) + Express Document SDK (for document APIs)
SDK Import:

Copied to your clipboard
import addOnSandboxSdk from "add-on-sdk-document-sandbox"; // Document Sandbox SDK
import { editor } from "express-document-sdk"; // Express Document SDK

Primary Role: Create/modify document elements, read document properties, process data securely

Communication Flow

From iframe Runtime to Document Sandbox

When you want to manipulate the document from your UI:

iframe Runtime (index.js)

Copied to your clipboard
/**
* ENVIRONMENT: iframe Runtime (UI)
* FILE: src/ui/index.js
* PURPOSE: Handle user interactions and trigger document operations
* SDK: Add-on UI SDK
*/
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";
// Wait for SDK to be ready before accessing runtime
addOnUISdk.ready.then(async () => {
const { runtime } = addOnUISdk.instance;
// Button click handler - responds to user action
document.getElementById("createText").addEventListener("click", async () => {
// Step 1: Get a proxy to call document sandbox functions
const sandboxProxy = await runtime.apiProxy("documentSandbox");
// Step 2: Call the function exposed in code.js
await sandboxProxy.createTextElement("Hello World!");
});
});

Document Sandbox (code.js)

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox (Isolated)
* FILE: src/sandbox/code.js
* PURPOSE: Perform document manipulation operations
* SDKs: Document Sandbox SDK (communication) + Express Document SDK (document APIs)
*/
import addOnSandboxSdk from "add-on-sdk-document-sandbox"; // For communication
import { editor } from "express-document-sdk"; // For document manipulation
const { runtime } = addOnSandboxSdk.instance;
// Expose functions that iframe runtime can call
runtime.exposeApi({
createTextElement: function(text) {
const textNode = editor.createText(text);
editor.context.insertionParent.children.append(textNode);
}
});

From Document Sandbox to iframe Runtime

When you want to update the UI from document operations:

Document Sandbox (code.js)

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox (Isolated)
* FILE: src/sandbox/code.js
* PURPOSE: Process document and send updates to UI
* SDK: Document Sandbox SDK (for communication back to UI)
*/
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
const { runtime } = addOnSandboxSdk.instance;
async function processDocument() {
// Step 1: Get a proxy to call UI functions
const uiProxy = await runtime.apiProxy("panel");
// Step 2: Update UI with progress
await uiProxy.updateProgress(50);
// Step 3: Perform document manipulation here...
// Step 4: Notify UI when complete
await uiProxy.updateProgress(100);
}

iframe Runtime (index.js)

Copied to your clipboard
/**
* ENVIRONMENT: iframe Runtime (UI)
* FILE: src/ui/index.js
* PURPOSE: Expose UI update functions that document sandbox can call
* SDK: Add-on UI SDK
*/
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";
addOnUISdk.ready.then(() => {
const { runtime } = addOnUISdk.instance;
// Expose functions that document sandbox can call
runtime.exposeApi({
updateProgress: function(percentage) {
// Update the DOM directly (iframe has full browser access)
const progressBar = document.getElementById("progress");
progressBar.style.width = percentage + "%";
}
});
});

Manifest Configuration

Your add-on's manifest.json defines the runtime structure and enables communication between environments.

Copied to your clipboard
/**
* FILE: manifest.json
* PURPOSE: Define add-on structure and entrypoints
*/
{
"entryPoints": [
{
"type": "panel", // Creates a panel in the Adobe Express UI
"id": "panel1", // Unique panel identifier
"main": "index.html", // UI entry point (iframe runtime)
"documentSandbox": "code.js" // Document sandbox code (optional)
}
]
}

Understanding runtime.apiProxy() Strings:

The string you pass to runtime.apiProxy() specifies which runtime you want to call:

  • From iframe runtime: Use runtime.apiProxy("documentSandbox") to call the document sandbox
    • The string "documentSandbox" is a fixed constant, not from your manifest
    • This works when you have "documentSandbox": "code.js" in your manifest
  • From document sandbox: Use runtime.apiProxy("panel") to call back to the iframe runtime
    • The string "panel" matches the "type": "panel" from your manifest
    • This lets your sandbox code communicate with the UI

Using RuntimeType Constants (Recommended):

Instead of string literals, you can import RuntimeType constants to avoid typos:

Copied to your clipboard
// In iframe runtime (index.js)
import addOnUISdk, { RuntimeType } from "https://express.adobe.com/static/add-on-sdk/sdk.js";
const sandboxProxy = await runtime.apiProxy(RuntimeType.documentSandbox);
// Instead of: runtime.apiProxy("documentSandbox")
Copied to your clipboard
// In document sandbox (code.js)
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
import { RuntimeType } from "express-document-sdk";
const uiProxy = await runtime.apiProxy(RuntimeType.panel);
// Instead of: runtime.apiProxy("panel")

Key Points:

  • "documentSandbox": "code.js" in manifest enables the document sandbox runtime
  • If documentSandbox is omitted, only the iframe runtime runs (UI-only add-on)
  • Use RuntimeType constants for type safety and to avoid string typos

iframe Runtime Overview

The iframe runtime is where your add-on's UI lives. It has full browser access and handles all user interactions.

What You Need to Import

Copied to your clipboard
/**
* ENVIRONMENT: iframe Runtime
* FILE: src/ui/index.js or index.html
* SDK: Add-on UI SDK
*/
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";
// Always wait for SDK to be ready
addOnUISdk.ready.then(() => {
const { runtime, app } = addOnUISdk.instance;
// runtime - for communication with document sandbox
// app - for UI SDK features (dialogs, OAuth, renditions, etc.)
});

Key Capabilities

  • Full browser access: DOM manipulation, fetch, localStorage, etc.
  • UI components: Render HTML, CSS, JavaScript frameworks
  • Add-on UI SDK features:
    • app.document.addImage() - Import media
    • app.document.createRenditions() - Export document
    • app.showModalDialog() - Display dialogs
    • app.oauth - Authentication flows
    • instance.clientStorage - Persistent storage
  • Communication: Use runtime.apiProxy("documentSandbox") to call sandbox functions

When Do I Need Document Sandbox Communication?

Document Sandbox Overview

The document sandbox provides a secure, isolated execution context for document manipulation.

What You Need to Import

Option 1: Communication Only (Document Sandbox SDK)

Use when you need to communicate with iframe runtime but NOT manipulate the document.

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox
* USE CASE: Data processing, calculations, transformations
* SDK: Document Sandbox SDK only
*/
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
const { runtime } = addOnSandboxSdk.instance;
runtime.exposeApi({
processData: function(data) {
// Process data without touching document
return data.map(item => item.toUpperCase());
}
});

Option 2: Communication + Document Manipulation (Both SDKs)

Most common pattern - Use when iframe runtime triggers document operations.

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox
* USE CASE: Create/modify document elements, read document properties
* SDKs: Document Sandbox SDK (communication) + Express Document SDK (document APIs)
*/
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
import { editor, colorUtils, constants, fonts } from "express-document-sdk";
const { runtime } = addOnSandboxSdk.instance;
runtime.exposeApi({
createShape: function() {
// Create and manipulate document elements
const rectangle = editor.createRectangle();
rectangle.width = 100;
rectangle.height = 100;
editor.context.insertionParent.children.append(rectangle);
},
analyzeDocument: function() {
// Read document properties
return {
pageCount: editor.documentRoot.pages.length,
selection: editor.context.selection.length
};
}
});

What's Available Without Import

The document sandbox automatically provides limited browser APIs. For complete details, see Web APIs Reference.

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox
* AVAILABLE: Global APIs (no import needed)
* NOTE: These are automatically injected into the sandbox environment
*/
// Console APIs - for debugging
console.log("Debugging output");
console.error("Error logging");
console.warn("Warnings");
console.assert(condition, "Assertion message");
// Blob interface - for binary data handling
const blob = new Blob(['data'], { type: 'text/plain' });
blob.text().then(text => console.log(text));
// Basic timing APIs (limited functionality)
setTimeout(() => { /* code */ }, 1000);
clearTimeout(timerId);

Environment Characteristics

  • Isolated JavaScript context: Secure sandbox execution
  • Limited browser APIs: Only essential APIs like console and Blob
  • Performance: Slower than iframe runtime but secure
  • No DOM access: Must communicate through iframe runtime for UI updates

Debugging Tips

Copied to your clipboard
/**
* ENVIRONMENT: Document Sandbox
* PURPOSE: Debug document sandbox code
* LIMITATION: No browser DevTools - use console for debugging
*/
// Basic debugging - view variable values
console.log("Variable:", myVariable);
// Object inspection - serialize to see structure
console.log("Object:", JSON.stringify(myObject, null, 2));
// Error tracking - catch and log errors
try {
const result = editor.createText("Hello");
console.log("Success:", result);
} catch (error) {
console.error("Failed:", error.message);
console.error("Stack:", error.stack);
}
// Performance debugging - measure execution time
const start = performance.now();
// ... your code ...
console.log(`Took ${performance.now() - start}ms`);

Common Patterns

Now that you understand both runtimes, here are the most common patterns you'll use:

Pattern 1: UI-Triggered Document Operations

Most common - User clicks button in UI → Create/modify document elements.

Copied to your clipboard
/**
* PATTERN: UI-Triggered Document Operations
* FLOW: User Action (iframe) → API Call → Document Change (sandbox)
* FILES: index.js + code.js
*/
// index.js (iframe Runtime)
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";
addOnUISdk.ready.then(async () => {
const { runtime } = addOnUISdk.instance;
const sandboxProxy = await runtime.apiProxy("documentSandbox");
document.getElementById("createBtn").onclick = async () => {
await sandboxProxy.createTextElement("Hello!");
};
});
// code.js (Document Sandbox)
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
import { editor } from "express-document-sdk";
const { runtime } = addOnSandboxSdk.instance;
runtime.exposeApi({
createTextElement: function(text) {
const textNode = editor.createText(text);
editor.context.insertionParent.children.append(textNode);
}
});

Pattern 2: Document-Driven UI Updates

Read document properties → Display in UI.

Copied to your clipboard
/**
* PATTERN: Document-Driven UI Updates
* FLOW: Document Read (sandbox) → Send Data → UI Update (iframe)
* FILES: index.js + code.js
*/
// index.js (iframe Runtime)
addOnUISdk.ready.then(async () => {
const { runtime } = addOnUISdk.instance;
const sandboxProxy = await runtime.apiProxy("documentSandbox");
const stats = await sandboxProxy.getDocumentStats();
document.getElementById("stats").textContent =
`Pages: ${stats.pageCount}, Elements: ${stats.elementCount}`;
});
// code.js (Document Sandbox)
runtime.exposeApi({
getDocumentStats: function() {
return {
pageCount: editor.documentRoot.pages.length,
elementCount: editor.context.insertionParent.children.length
};
}
});

Pattern 3: Bi-directional Communication

Document sandbox calls back to UI to show progress.

Copied to your clipboard
/**
* PATTERN: Bi-directional Communication
* FLOW: UI → Sandbox → Document + Sandbox → UI (progress updates)
* FILES: index.js + code.js
*/
// index.js (iframe Runtime)
addOnUISdk.ready.then(async () => {
const { runtime } = addOnUISdk.instance;
// Expose progress handler
runtime.exposeApi({
updateProgress: (percent) => {
document.getElementById("progress").style.width = percent + "%";
}
});
const sandboxProxy = await runtime.apiProxy("documentSandbox");
await sandboxProxy.processLargeOperation();
});
// code.js (Document Sandbox)
runtime.exposeApi({
processLargeOperation: async function() {
const uiProxy = await runtime.apiProxy("panel");
await uiProxy.updateProgress(25);
// ... do work ...
await uiProxy.updateProgress(50);
// ... more work ...
await uiProxy.updateProgress(100);
}
});

Best Practices

1. Clear File Organization

  • Keep UI logic in index.js
  • Keep document logic in code.js
  • Use consistent naming for exposed APIs
  • Separate concerns clearly between environments

2. Error Handling

Always handle errors gracefully in both environments:

Copied to your clipboard
/**
* BEST PRACTICE: Error Handling
* APPLIES TO: Both iframe runtime and document sandbox
* WHY: Graceful failures improve user experience and aid debugging
*/
// iframe Runtime: Wait for SDK ready state
addOnUISdk.ready.then(() => {
const { runtime } = addOnUISdk.instance;
// Safe to use runtime now
}).catch(error => {
console.error("SDK initialization failed:", error);
});
// Document Sandbox: Wrap API calls in try-catch
runtime.exposeApi({
createText: function(text) {
try {
// Direct document editing
const textNode = editor.createText(text);
editor.context.insertionParent.children.append(textNode);
return { success: true };
} catch (error) {
// Log error for debugging
console.error("Text creation failed:", error.message);
// Return error info to caller
return { success: false, error: error.message };
}
}
});

Key practices:

  • Always wrap API calls in try-catch blocks
  • Provide meaningful error messages to users
  • Handle communication failures gracefully
  • Use console.error() for debugging in sandbox
  • Validate inputs before processing

3. Performance

  • Minimize data transfer between environments
  • Batch multiple operations when possible
  • Use appropriate data types (see Communication APIs)
  • Remember document sandbox runs slower than iframe runtime
  • Avoid frequent cross-runtime communication in loops

4. Debugging

  • Use console.log in both environments
  • Check browser dev tools for iframe runtime
  • Use sandbox console for document sandbox debugging
  • Use console.assert() for validation checks
  • Test communication flows thoroughly

5. Security

  • Never expose sensitive data through communication APIs
  • Validate all data received from the UI
  • Use the sandbox's isolation as intended
  • Don't attempt to bypass security restrictions

FAQs

Q: Why are there two different runtime objects?

A: Each environment has its own runtime object that acts as a "communication phone." The iframe runtime calls the document sandbox, and the document sandbox calls the iframe runtime. This separation ensures security and proper isolation.

Q: When do I use addOnUISdk.instance.runtime vs addOnSandboxSdk.instance.runtime?

A: Use the runtime object from the environment you're currently in:

  • In index.js (iframe runtime): Use addOnUISdk.instance.runtime (from Add-on UI SDK)
  • In code.js (document sandbox): Use addOnSandboxSdk.instance.runtime (from Document Sandbox SDK)

Q: What does await runtime.apiProxy("documentSandbox") actually do?

A: It creates a proxy object that lets you call functions you've exposed in the document sandbox from your iframe runtime code. Think of it as getting a "remote control" for the other environment.

Q: What's the difference between "documentSandbox" and "panel" in apiProxy?

A: These specify which environment you want to communicate with:

  • "documentSandbox" - Call from iframe runtime to document sandbox
  • "panel" - Call from document sandbox to iframe runtime

Q: Can I access Document APIs directly from the iframe runtime?

A: No, Document APIs (Express Document SDK) are only available in the document sandbox for security reasons. You must use the communication system (Document Sandbox SDK) to bridge between environments.

Q: How do I know which environment my code is running in?

A: Check your file structure and imports:

  • If you're importing addOnUISdk (Add-on UI SDK) → You're in the iframe runtime
  • If you're importing addOnSandboxSdk (Document Sandbox SDK) → You're in the document sandbox

Q: Do I always need both imports in my code.js file?

A: No! It depends on what your add-on does:

  • Communication only: Just Document Sandbox SDK (addOnSandboxSdk) - processing data without document changes
  • Communication AND document manipulation: Both SDKs (Document Sandbox SDK + Express Document SDK) - most common pattern
  • You typically need both if you're creating/modifying document content based on UI interactions

Q: When does my document sandbox code (code.js) actually execute?

A: Your code.js only executes when your add-on's panel is opened by the user. Specifically:

  • If you have a panel entrypoint with documentSandbox: "code.js" in your manifest, the code.js file loads when the user opens your add-on panel
  • The code doesn't run automatically on document load or in the background
  • It only executes in the context of your add-on being actively used
  • This ensures security and performance by only running code when needed

Q: What else is available in the Document Sandbox SDK package?

A: Besides runtime, the Document Sandbox SDK provides:

  • Web APIs: Limited browser APIs like console and Blob for debugging and data handling
  • Automatic global injections: No need to import basic APIs like console
  • Secure execution environment: Isolated JavaScript context with performance monitoring
  • Communication infrastructure: Handles the complex proxy system between environments

Q: Can I use fetch() or other network APIs in the document sandbox?

A: No, network APIs like fetch() are not available in the document sandbox for security reasons. If you need to make network requests, do them in the iframe runtime and pass the data to the document sandbox via communication APIs (Document Sandbox SDK).

Q: Can my code.js file run without a UI panel?

A: No, for third-party add-ons, the document sandbox (code.js) only runs in the context of a panel entrypoint. Your add-on must have a UI (panel) that users interact with, and the document sandbox code executes to support that UI's document manipulation needs.

Q: Why does my add-on feel slower in the document sandbox?

A: The document sandbox runs in an isolated environment (QuickJS) which is inherently slower than the native browser JavaScript engine. This is by design for security. Minimize complex operations and data transfer between environments.


Next Steps

Now that you understand the architecture, explore these guides and tutorials:

  • Privacy
  • Terms of Use
  • Do not sell or share my personal information
  • AdChoices
Copyright © 2025 Adobe. All rights reserved.