# Architecture — `ui-extensions-sdk`
This repository builds the **App SDK** and publishes it to npm as `@contentful/app-sdk` (preferred) and `contentful-ui-extensions-sdk` (legacy alias). Everything below describes the codebase in this repo and the runtime it ships.
## Overview
The App SDK is the **bridge** between an app running in a sandboxed iframe (the *child frame*, customer code) and the Contentful Web App (the *parent frame*, host). It exposes a typed, Promise-based JavaScript API that wraps PostMessage interactions with the host. Apps call `init()`, receive a location-specific SDK object, and use it to read/write content, open dialogs, navigate, and (in newer surfaces) interact with AI agents.
Inception: 2015-11-02 (`d21d1f9` `Initial commit`). Core transport pattern is unchanged; new locations and APIs are layered on top.
## System Context
```mermaid
graph LR
CWA["Contentful Web App
(parent frame)"]
WR["Widget Renderer"]
IF["Customer App iframe
(child frame)"]
SDK["@contentful/app-sdk
(this repo)"]
CMA["Contentful Management API"]
IF -.uses.-> SDK
CWA --> WR
WR -- "postMessage(connect)" --> IF
IF -- "postMessage(call/send)" --> CWA
CWA -- "postMessage(response/event)" --> IF
SDK -- "via channel" --> CMA
```
The SDK never talks to Contentful APIs directly. CMA calls flow through the channel: `app code → sdk.cma.() → cmaAdapter → channel.call('CMAAdapterCall', opts) → web app → CMA`. This indirection lets the host inject auth, environment, and release context without the app handling tokens.
## Module Map (`lib/`)
| Module | Role | Key surface |
|---|---|---|
| `index.ts` | Public entry: re-exports types and exposes `init` + `locations` | `init`, `locations`, all public types |
| `initialize.ts` | Listens for the host's `connect` message, builds the SDK, and hands it to the user's callback | `createInitializer` |
| `channel.ts` | PostMessage send/receive; correlates request IDs to response promises; dispatches host events to handlers | `Channel`, `connect`, `sendInitMessage` |
| `signal.ts` | Tiny event emitter; `MemoizedSignal` replays the latest value to new subscribers | `Signal`, `MemoizedSignal` |
| `api.ts` | Composes the location-specific SDK by reducing a list of "producer" functions | `createAPI`, `LOCATION_TO_API_PRODUCERS` |
| `locations.ts` | The `locations` runtime enum exported to consumers | `LOCATION_*` keys |
| `entry.ts` | Entry-level API (`getSys`, `publish`, `save`, `onSysChanged`, fields, tasks, metadata) | `createEntry` |
| `field.ts` | Per-content-type field; multi-locale wrapper | `makeField` |
| `field-locale.ts` | Single field+locale: `getValue`, `setValue`, `onValueChanged`, validation/disabled signals | `makeFieldLocale` |
| `editor.ts` | Entry editor settings: locale settings, hidden-field visibility | `createEditor` |
| `space.ts` | Legacy CRUD wrappers (deprecated since v4.0.0) — every method warns and delegates to channel | `createSpace` |
| `dialogs.ts` | System dialogs (alert/confirm/prompt) and entity selectors (entries, assets, experiences, patterns, component definitions) | `createDialogs` |
| `navigator.ts` | Open/navigate to entries, assets, app pages, app config; release-aware | `createNavigator` |
| `app.ts` | App lifecycle hooks for the config screen: `setReady`, `onConfigure` (preInstall), `onConfigurationCompleted` (postInstall) | `createApp` |
| `agent.ts` | AI agent location: context updates, toolbar actions, layout variant control | `createAgent` |
| `asset.ts` | Asset-side methods on the asset sidebar location | `createAsset` |
| `cma.ts` | Wraps `contentful-management` `createClient` with the channel-backed adapter | `createCMAClient` |
| `cmaAdapter.ts` | The channel-backed `Adapter` for `contentful-management` | `createAdapter` |
| `window.ts` | iframe height management: `MutationObserver`-driven auto-resize + manual `updateHeight` | `createWindow` |
| `types/` | Public TypeScript surface — no runtime code | All `*AppSDK` types, `ConnectMessage`, `IdsAPI`, etc. |
| `utils/deferred.ts` | Promise-with-external-resolve helper used by `initialize.ts` | `createDeferred` |
## Data Flow: Initialization Handshake
```
[Host] [App iframe]
│ │
│ ◄─── connect listener ──────│ channel.ts:waitForConnect
│ │ installs message listener
│ ◄── postMessage(init) ──────│ channel.ts:sendInitMessage
│ │
│── postMessage("connect", [params, queue]) ─► │ initialize.ts:connectDeferred resolves
│ │ api.ts:createAPI builds SDK
│ │ user's init callback invoked
│ │
│── postMessage() ────────────────►│ channel.ts:_handleMessage routes
```
The SDK *connects immediately on import*, before `init()` is called, so it doesn't lose host messages that arrive before the app is ready (`initialize.ts:21-32`). User callback fires after the connect handshake completes.
## Data Flow: A Field Read
```
app code field-locale.ts channel.ts host
│ getValue() ────►│ │ │
│ │ returns memoized value │ │
│◄────────────────│ │ │
...
host emits valueChanged event
│ │ ◄── addHandler ─────────│ │
│ onValueChanged ─│ │ │
│ (handler) │ │ ◄── postMessage ─│
│ │ signal.dispatch │ │
│◄── handler ─────│ │ │
```
Reads of "current value" are local (memoized in `MemoizedSignal`); writes (`setValue`) and most other operations round-trip through the channel.
## Build Pipeline
```
lib/**/*.ts
│
├─► tsc --noEmit (npm run check-types — gate)
│
└─► rollup (rollup.config.js)
├─► dist/cf-extension-api.js UMD, target=ES5, terser-minified
├─► dist/cf-extension-api.bundled.js UMD with deps inlined for CDN consumption
├─► dist/index.d.ts tsc-emitted ambient types
└─► dist/cf-extension.css (legacy stylesheet, kept for back-compat)
```
- **Target ES5** for broadest browser compatibility (`tsconfig.json#target`); customers loading via `