> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# App Shell & UI Integration

> How to integrate your app with the JTL App Shell using the manifest and AppBridge communication

This guide covers the two systems that control how your Cloud App renders and communicates inside the JTL platform:

* **Manifest**: Configuration document with two sections. The `manifest` section defines lifecycle hooks, capabilities, and API scopes. The `listing` section defines name, description, icons, legal URLs, and support links shown in the JTL Hub and App Store.
* **AppBridge**: Runtime SDK that enables bidirectional communication between your app (in an iframe) and the host environment (JTL Hub or ERP Cloud).
* **Platform UI**: React component library for building interfaces that match JTL's design system.

For authentication flows, see [Authentication & Login](/guides/cloud-apps/authentication-login). For calling the JTL Cloud and JTL-Wawi APIs, see [Using Platform APIs](/guides/cloud-apps/using-platform-apis).

## App Manifest

The manifest is a JSON document that defines your app. It has two sections that work together: `manifest` for technical runtime configuration, and `listing` for the marketing content shown in the App Store and JTL Hub. You need both to deploy an app through the [Partner Portal](https://partner.jtl-cloud.com/).

```mermaid theme={null}
flowchart LR
    M["App Manifest"] --> Tech["manifest section
    (technical runtime)"]
    M --> List["listing section
    (marketing & UI)"]

    Tech --> Platform["JTL Platform"]
    List --> Platform

    Platform --> Hub["JTL Hub"]
    Platform --> ERP["JTL ERP"]
    Platform --> Store["App Store"]

    style M fill:#FFF2EB,stroke:#FB581F,stroke-width:2px,color:#0B1B45
    style Tech fill:#FFFFFF,stroke:#FB581F,stroke-width:1px,color:#0B1B45
    style List fill:#FFFFFF,stroke:#89D2FF,stroke-width:1px,color:#0B1B45
    style Platform fill:#EEEEE7,stroke:#0B1B45,stroke-width:2px,color:#0B1B45
    style Hub fill:#FFFFFF,stroke:#0B1B45,stroke-width:1px,color:#0B1B45
    style ERP fill:#FFFFFF,stroke:#0B1B45,stroke-width:1px,color:#0B1B45
    style Store fill:#FFFFFF,stroke:#0B1B45,stroke-width:1px,color:#0B1B45
```

### Manifest Section

The `manifest` section holds technical runtime configuration: identity, lifecycle hooks, and capabilities.

#### Top-level Fields

Two fields identify the app and its release.

| Property        | Type   | Required | Description                                                                                        |
| --------------- | ------ | -------- | -------------------------------------------------------------------------------------------------- |
| `technicalName` | string | Yes      | Stable internal identifier for the app. Max 100 characters. Used as a fallback display name.       |
| `version`       | string | Yes      | App release version. Strict semver, digits only (e.g., `1.0.0`). Pre-release tags are not allowed. |

#### Requirements

Declare the minimum platform API version your app needs. JTL prevents installation on environments that don't meet the requirement.

| Property                          | Type   | Required | Description                          |
| --------------------------------- | ------ | -------- | ------------------------------------ |
| `requirements.minCloudApiVersion` | string | No       | Minimum compatible Cloud API version |

#### Lifecycle

The lifecycle object defines the URL JTL loads when a merchant installs your app.

| Property                     | Type   | Required | Description                                                                                                                                                |
| ---------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lifecycle.configurationUrl` | string | No       | URL rendered in an iframe during initial setup or onboarding. Use this to present UI, complete the AppBridge handshake, and persist the tenant connection. |

The URL must be a standard `http://` or `https://` URL and is validated against the pattern `^https?:\/\/(?:[^{}\s]|\{\s*metadata\.[a-zA-Z0-9_-]+\s*})+$` when you register your manifest.

**Identifying the App**

If you publish multiple apps, each one needs its own `configurationUrl`. Two apps sharing the same URL will cause the wrong setup UI to be rendered when a merchant installs either of them.

Give each app a distinct URL, either by using separate routes:

```json theme={null}
"lifecycle": {
  "configurationUrl": "https://example.com/setup/inventory-sync"
}
```

Or by encoding the app's identity in the query string:

```json theme={null}
"lifecycle": {
  "configurationUrl": "https://example.com/setup?app=inventory-sync"
}
```

Both approaches are valid. Use separate routes if you control routing and want cleaner URLs. Use query parameters if you prefer a single handler that switches behavior dynamically.

<Tip>
  A working configuration flow is provided when you use the CLI `npm create
    	@jtl-software/cloud-app@latest` to create a new app.
</Tip>

#### Capabilities

Capabilities define where and how your app integrates with JTL. Each key under `capabilities` maps to a specific surface. See [Architecture Overview](/guides/cloud-apps/architecture-overview) for a full description of each integration type.

**Hub**

Controls how the app appears in the JTL Hub dashboard. When a merchant clicks the app's card, the platform redirects to this URL.

| Property                                   | Type   | Required                     | Description                                                         |
| ------------------------------------------ | ------ | ---------------------------- | ------------------------------------------------------------------- |
| `capabilities.hub.appLauncher.redirectUrl` | string | Required if `hub` is present | URL the system redirects to when the app card is clicked in the Hub |

Without a `redirectUrl`, the app card has no destination.

**ERP: Menu Items**

Add entries to the ERP Cloud sidebar under the **App** section. Each menu item links to a page in your app loaded inside the ERP iframe.

| Property | Type   | Required | Description                                  |
| -------- | ------ | -------- | -------------------------------------------- |
| `id`     | string | Yes      | Stable internal identifier for the menu item |
| `name`   | string | Yes      | Display name shown in the navigation         |
| `url`    | string | Yes      | URL loaded when the item is clicked          |

**ERP: API Scopes**

Declare the JTL-Wawi API resources your app intends to use. Declared scopes are validated against the allowed list at registration.

<Note>
  Runtime enforcement of these scopes by the API layer has not shipped yet, so declaring a scope here does not currently restrict what your app can call. This is a temporary state. Once enforcement is live, the API will reject calls that exceed the declared set, and the merchant will see the declared scopes at install time. See [Scopes & Permissions](/guides/essentials/authentication/scopes-permissions) for the full list and roadmap context.
</Note>

| Property                      | Type            | Required | Description                               |
| ----------------------------- | --------------- | -------- | ----------------------------------------- |
| `capabilities.erp.api.scopes` | array of string | No       | API permission scopes required by the app |

The platform accepts only 41 specific scope values. Any value outside this list is rejected at registration.

A subset for reference:

```json theme={null}
"scopes": [
  "items.read", "items.write",
  "orders.read", "orders.write",
  "customers.read", "customers.write",
  "inventory.read", "inventory.write"
]
```

**Pane**

Add sidebar panels that appear alongside ERP views. Panes are context-aware and show up only on matching pages.

| Property            | Type            | Required | Description                                                                                                            |
| ------------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `id`                | string          | No       | Internal identifier for the pane                                                                                       |
| `context`           | string          | Yes      | Dot-notation path where the pane appears. The path mirrors the route: `/customers/details` becomes `customers.details` |
| `matchChildContext` | boolean         | No       | If `true`, the pane also matches subpaths of the context (e.g., `customers` matches `customers.details`)               |
| `title`             | string          | Yes      | Title shown in the pane header and in the pane dropdown                                                                |
| `url`               | string          | Yes      | URL loaded inside the pane iframe                                                                                      |
| `requiredScopes`    | array of string | No       | Per-pane scope override. Uses the same 41-value enum as `capabilities.erp.api.scopes`                                  |

### Listing Section

The `listing` section holds the marketing content shown to merchants in the App Store and JTL Hub: localized names and descriptions, icons, support URLs, and legal links.

#### Localized Name and Description

Names and descriptions are keyed by locale. The `defaultLocale` is the fallback when a merchant's preferred locale is not available.

| Property                     | Type   | Description                                                       |
| ---------------------------- | ------ | ----------------------------------------------------------------- |
| `defaultLocale`              | string | Fallback locale (e.g., `de-DE`)                                   |
| `name.{locale}.short`        | string | Short name shown in lists and cards. **Max 30 characters**.       |
| `name.{locale}.full`         | string | Full name shown on the app detail page. **Max 80 characters**.    |
| `description.{locale}.short` | string | Teaser shown in cards. **Max 200 characters**.                    |
| `description.{locale}.full`  | string | Full description shown on the app detail page. **No max length**. |

#### Media

Icons render in light and dark themes. Both URLs must be publicly reachable at registration and during installation.

| Property            | Type   | Description                                     |
| ------------------- | ------ | ----------------------------------------------- |
| `media.icons.light` | string | Icon URL for light backgrounds (HTTPS, PNG/SVG) |
| `media.icons.dark`  | string | Icon URL for dark backgrounds (HTTPS, PNG/SVG)  |

#### Support and Documentation

Links shown on the app detail page. Both fields are localized.

| Property                         | Type   | Description                |
| -------------------------------- | ------ | -------------------------- |
| `support.url.{locale}`           | string | Support link (HTTPS)       |
| `support.documentation.{locale}` | string | Documentation link (HTTPS) |

#### Legal

Required legal URLs for compliance. GDPR endpoints accept data access and deletion requests.

| Property              | Type   | Description                         |
| --------------------- | ------ | ----------------------------------- |
| `legal.privacyPolicy` | string | URL to the privacy policy           |
| `legal.termsOfUse`    | string | URL to the terms of service         |
| `legal.gdpr.request`  | string | URL for GDPR data access requests   |
| `legal.gdpr.delete`   | string | URL for GDPR data deletion requests |

### Complete Manifest Example

The example below shows both sections together: a manifest declaring Hub redirect, one ERP menu item, three API scopes, and one pane scoped to the orders view, alongside a listing covering the German locale.

```json theme={null}
{
  "manifest": {
    "technicalName": "my-example-app",
    "version": "1.0.0",
    "requirements": {
      "minCloudApiVersion": "1.0.0"
    },
    "lifecycle": {
      "configurationUrl": "https://example.com/configure"
    },
    "capabilities": {
      "hub": {
        "appLauncher": {
          "redirectUrl": "https://example.com/hub/launch"
        }
      },
      "erp": {
        "menuItems": [
          {
            "id": "menu-item-1",
            "name": "Mein Menüpunkt",
            "url": "https://example.com/erp/menu"
          }
        ],
        "api": {
          "scopes": ["orders.read", "orders.write", "items.read"]
        },
        "pane": [
          {
            "id": "pane-1",
            "context": "orders",
            "matchChildContext": false,
            "title": "Bestellübersicht",
            "url": "https://example.com/erp/pane/orders",
            "requiredScopes": ["orders.read"]
          }
        ]
      }
    }
  },
  "listing": {
    "version": "1.0.0",
    "defaultLocale": "de-DE",
    "name": {
      "de-DE": {
        "short": "Hello World example",
        "full": "Hello World example"
      }
    },
    "description": {
      "de-DE": {
        "short": "Official Hello World example.",
        "full": "This is the official Hello World example."
      }
    },
    "media": {
      "icons": {
        "light": "https://picsum.photos/200/200",
        "dark": "https://picsum.photos/200/200"
      }
    },
    "support": {
      "url": {
        "de-DE": "https://support.example.com/tickets"
      },
      "documentation": {
        "de-DE": "https://docs.example.com/user-guide"
      }
    },
    "legal": {
      "privacyPolicy": "https://example.com/legal/privacy",
      "termsOfUse": "https://example.com/legal/terms",
      "gdpr": {
        "request": "https://example.com/gdpr/data-request",
        "delete": "https://example.com/gdpr/account-deletion"
      }
    }
  }
}
```

***

## AppBridge Communication

The AppBridge is provided by the [`@jtl-software/cloud-apps-core`](https://www.npmjs.com/package/@jtl-software/cloud-apps-core) package. It establishes a secure communication channel between your app's frontend (running in an iframe) and the host JTL environment (JTL Hub or ERP Cloud).

The bridge allows your app to request data (like session tokens) and call host methods. Publish/subscribe event handling is in development ([see below](/guides/cloud-apps/app-shell-ui-integration#events)).

### Installation

```bash theme={null}
npm install @jtl-software/cloud-apps-core
```

### Initializing the AppBridge

Initialize the AppBridge **before** rendering your React app, then pass the instance as a prop or store it in context:

```tsx theme={null}
import { createAppBridge } from '@jtl-software/cloud-apps-core';
import { createRoot } from 'react-dom/client';

createAppBridge().then((appBridge) => {
	createRoot(document.getElementById('root')!).render(
		<StrictMode>
			<App appBridge={appBridge} />
		</StrictMode>,
	);
});
```

**What this does:** Calls `createAppBridge()` to establish the iframe-to-host connection, waits for the bridge to be ready, then passes it to your app as a prop. This guarantees the communication channel is open before any component tries to use it.

<Warning>
  Do not initialize the AppBridge inside a component (e.g., in `useEffect`).
  This can cause race conditions where components render before the bridge is
  ready. Always initialize it at the application entry point.
</Warning>

For Next.js apps that need SSR compatibility, use a dynamic import inside a
client-side provider instead. See the [From Scratch
Quickstart](/get-started/quick-start/from-scratch#appbridge-provider) for the
full pattern.

<Tip>
  You can use AppBridge in any JavaScript frontend framework, including React,
  Vue, and Angular.
</Tip>

### Communication Flow

The AppBridge creates a two-way channel: your app's `appBridge` instance communicates with a corresponding `hostAppBridge` instance inside the JTL Cloud service.

```mermaid theme={null}
sequenceDiagram
    participant App as App Frontend (iframe)
    participant Bridge as appBridge
    participant Host as hostAppBridge
    participant Service as JTL Cloud Service

    App->>Bridge: Initialize (createAppBridge())
    activate Bridge
    Bridge->>Host: Establish connection
    activate Host
    Host-->>Bridge: Connection ready
    deactivate Host
    Bridge-->>App: Bridge instance ready
    deactivate Bridge

    Note over App, Service: Runtime interactions

    App->>Bridge: method.call("getSessionToken")
    activate Bridge
    Bridge->>Host: Invoke getSessionToken()
    activate Host
    Host->>Service: Execute host logic
    activate Service
    Service-->>Host: Return result
    deactivate Service
    Host-->>Bridge: Forward result
    deactivate Host
    Bridge-->>App: Return session token
    deactivate Bridge
```

All communication is **asynchronous and non-blocking**. Your app continues executing while messages are delivered between the iframe and the host. There are no HTTP requests or polling involved.

### API Reference

The AppBridge provides two main interfaces: **methods** (expose/call) and **events** (publish/subscribe).

<Tabs>
  <Tab title="Methods">
    AppBridge methods provide a request/response channel between your app and the host. There are two directions:

    * **Call host methods** (available today): your app invokes functions the host exposes, like `getSessionToken`. See [Environment-specific Methods](#environment-specific-methods) for the full list.
    * **Expose methods to the host** (in development): your app registers functions that the host can invoke by name, used for things like custom calculations or data lookups.

    ### Calling Host Methods

    Invoke a function provided by JTL. The call returns a promise that resolves with the host's result.

    ```javascript theme={null}
    const sessionToken = await appBridge.method.call("getSessionToken");
    ```

    **What this does:** Sends a method call across the bridge. The host executes the function and returns the result as a resolved promise.

    ### Exposing Methods to the Host

    <Info>
      **In development:** `method.expose` and `method.isExposed` are in active development and not yet available. This section will be updated when the API ships.
    </Info>

    Once available, exposing methods will let the host invoke logic that lives in your app. For example, a `calculateShippingCost` function the ERP calls before showing a quote, or a `validateOrder` hook called before checkout. The shape will follow a `name + handler` registration pattern with a disposer for cleanup, mirroring the event subscription model.

    ### Method API Summary

    The table marks which APIs are available today and which are planned.

    | Method                       | Status         | Description                                    |
    | ---------------------------- | -------------- | ---------------------------------------------- |
    | `method.call(name, ...args)` | Available      | Call a host method. Returns `Promise<T>`.      |
    | `method.expose(name, fn)`    | In development | Register a function the host can call by name. |
    | `method.isExposed(name)`     | In development | Check whether a method is already registered.  |
  </Tab>

  <Tab title="Events">
    <Info>
      **In development:** AppBridge publish/subscribe events are in development and not yet available. This section will be updated when the API ships.
    </Info>

    AppBridge events will provide two capabilities:

    * **`event.subscribe`**: listen for events emitted by the host (for example, `inventory:updated` when a merchant changes stock elsewhere in the ERP).
    * **`event.publish`**: send events to the host so it can react to actions your app completes (for example, `order:verification:complete`).

    Topics will follow a `resource:action` naming convention so intent is clear from the name alone. Payloads will be small by design (IDs and changed values), not full objects.
  </Tab>
</Tabs>

### Environment-specific Methods

JTL exposes different built-in methods depending on which environment your app is running in.

<Tabs>
  <Tab title="Setup Environment (JTL Hub)">
    These methods are available when your app loads via the `lifecycle.configurationUrl` during installation:

    | Method            | Parameters | Returns           | Description                                                       |
    | ----------------- | ---------- | ----------------- | ----------------------------------------------------------------- |
    | `getSessionToken` | None       | `Promise<string>` | Returns the current session token (contains user and tenant info) |
    | `setupCompleted`  | None       | `Promise<void>`   | Signals that setup is done and activates the app in the Hub       |
  </Tab>

  <Tab title="ERP Environment (ERP Cloud)">
    These methods are available when your app runs inside the ERP (via menu items, tabs, or panes):

    | Method            | Parameters | Returns           | Description                               |
    | ----------------- | ---------- | ----------------- | ----------------------------------------- |
    | `getSessionToken` | None       | `Promise<string>` | Returns the current session token         |
    | `getCurrentTime`  | None       | `Promise<Date>`   | Returns the current time as a Date object |
  </Tab>
</Tabs>

<Note>
  `getSessionToken` is available in both environments. It's the primary way your frontend identifies the current user and tenant. For details on verifying session tokens on your backend, see [Authentication & Login](/guides/cloud-apps/authentication-login).
</Note>

***

## Platform UI Components

JTL provides a [React component library](https://www.npmjs.com/package/@jtl-software/platform-ui-react) that matches the platform's design system. Using these components ensures your app looks and feels consistent with the rest of the JTL interface.

### Installation

```bash theme={null}
npm install @jtl-software/platform-ui-react
```

Add the CSS import to your global stylesheet:

```css theme={null}
@import '@jtl-software/platform-ui-react/index.css';
```

### Available Components

The library includes form controls, layout containers, and typography components. Here's the full list:

**Layout components**

* `Box` , `Grid` , `Stack` , `Layout` , `LayoutSection` , `Card`

**Form components**

* `Button` , `Checkbox` , `Input` , `InputOTP` , `Radio` , `Select` , `Textarea` , `Switch` , `Toggle` , `ToggleGroup` , `FormGroup` , `Form`

**Data display**

* `Text` , `Badge` , `Avatar` , `Table` , `DataTable` , `Progress`

**Navigation**

* `Link` , `Breadcrumb` , `Tab` , `Dropdown`

**Feedback**

* `Alert` , `Dialog` , `AlertDialog` , `Tooltip` , `Skeleton`

**Utility**

* `Icon` , `Separator` , `ScrollArea` , `Collapsible` , `Popover` , `Sheet`

### Usage

Import components directly from the package:

```tsx theme={null}
import { Button, Card, Text } from '@jtl-software/platform-ui-react';

export default function MyComponent() {
	return (
		<Card>
			<Text>Welcome to my JTL App</Text>
			<Button
				onClick={() => console.log('clicked')}
				label='Get Started'
			/>
		</Card>
	);
}
```

**What this does:** Renders a card with JTL-styled typography and a button.

## What's Next

<CardGroup cols={2}>
  <Card title="Authentication & Login" icon="key" href="/guides/cloud-apps/authentication-login">
    Implement OAuth 2.0 and session token verification in your app.
  </Card>

  <Card title="Using Platform APIs" icon="database" href="/guides/cloud-apps/using-platform-apis">
    Call the JTL Cloud REST and GraphQL APIs from your backend.
  </Card>

  <Card title="Handling Webhooks" icon="bell" href="/guides/cloud-apps/handling-webhooks">
    Respond to events and AppBridge messages.
  </Card>

  <Card title="Best Practices" icon="star" href="/guides/cloud-apps/best-practices">
    Patterns for AppBridge initialization, error handling, and production
    readiness.
  </Card>
</CardGroup>
