# Custom Apps

The MCP **Custom App Guide** (`custom_app_guide`) guides execution-capable external AI through the public custom-app boilerplate to build, validate, and package purpose-built Widgets. A **Super Administrator** uploads platform apps through **Administrator > Apps**; on Business and Enterprise plans, customer **Owners** and **Administrators** manage their own Widget packages and HTML/CSS/JS bundles through **Settings > Apps**. A content designer places an enabled app. Optional editable `INTERNAL` `TABLE` datasources keep operational data separate from app code.

Custom apps are packaged applications placed as widgets in the Designer and run through the existing legacy editor/displayer custom-widget runtime. Their code defines presentation and behavior; Widgets can expose settings and bind datasources, while HTML/CSS/JS bundles have no app-defined Designer settings. Interactivity depends on the app; a custom app is not a complete content template.

## MCP-guided external generation

The guide returns read-only instructions. The external AI executes the build and validation; the guide does not build or upload packages.

1. Provide the external AI with requirements, branding assets, target screen dimensions, behavior, and representative data.
2. The AI needs a local or hosted workspace with filesystem, shell, Git, the Node.js version required by the boilerplate, npm/SDK access, browser-based validation, and artifact return. MCP chat access alone is insufficient.
3. Follow the boilerplate's current workflow: setup, generation brief and validation, implementation, realistic previews, behavior checks, visual review, and accepted delivery.
4. Return the upload ZIP, separate sanitized source ZIP, generation brief, visual review, and delivery manifest. Data-bound apps also include datasource contracts and sample data for each binding.
5. Install the app and create/select and bind its datasources through the existing Wallboard paths below.

Generation runs in the external AI environment using its AI subscription/API budget. There is no Wallboard-hosted coding service or in-product prompt/photo builder in this workflow.

## Install, enable, and bind

| Step / UI | Current behavior and permission |
|---|---|
| **Administrator > Apps** | **Super Administrator** creates platform app records, uploads accepted app ZIPs, and assigns customer availability. Upload the app ZIP, never the source ZIP. |
| **Settings > Apps** | Customer **Owners** and **Administrators** on Business and Enterprise plans create customer app records, choose **Widget** or **HTML/CSS/JS bundle**, upload a ZIP, and enable it. The catalogue and resolution are customer-scoped. |
| App availability | Upload is required before enablement. Platform apps can be **Available to all customers** or assigned to selected customers. |
| **Contents > Legacy Designer > Apps** | A user authorized to edit content places an enabled app in its widget category (**Custom apps** by default) and configures any exposed properties. Disabled apps are hidden from the Designer catalogue. |
| **Settings > Datasources > Internal Datasources** | Separately create/import the generated internal table datasource, or select an existing datasource matching the app's contract. Existing datasource permissions apply. |
| Designer app properties | Bind every declared datasource property with its data picker, following the delivered contract and the app's UI labels. Upload does not create or bind a datasource. |
| Ongoing data edits | Authorized datasource editors update operational rows in **Internal Datasources**. The host forwards bound-data changes to the running app without rebuilding its ZIP. Updates affect other content bound to the same datasource. |

Platform app-record defaults: type `WIDGET`, category `CUSTOM_APPS`, **Available to all customers** on, automatic updates off, enabled off. Select the intended platform-app customer scope before enabling. Customer app records start disabled with no uploaded bundle.

MCP `manage_datasource` can read/edit existing `INTERNAL` `TABLE`/`CUSTOM` datasource data, including table schemas and typed rows. It does not make packaged sidecars an automatic installer. MCP access and existing role/team permissions still apply.

## Editable data and binding contracts

- Preserve supplied schemas or verified integrated sources such as Feed/Calendar. For newly generated, user-maintained records, prefer an editable internal `TABLE`; static apps need no datasource picker.
- Backend `structureType: "TABLE"` describes datasource storage. Editor binding type is separate: a custom-app `dataPicker` normally uses `dataPickerType: "any"` so the app receives the selected value; `"table"` invokes specialized built-in Table-widget processing.
- Contract/sample files describe what to create or select and which property to bind. Wallboard stores packaged datasource metadata but does not provision or bind from it.
- Follow each app's declared input shape. Generated `TABLE` contracts document supported wrappers: a native datasource object containing the named table, the selected `{header, rows, connectors}` table object, or a processed row array. This is not arbitrary-JSON support for every app.
- The generation contract distinguishes unbound samples from bound-empty/null or invalid data, which must produce an explicit empty/error state. Required fields, filtering, ordering, and pagination belong to each app's contract. Routine data edits need no rebuild; layout/code changes require a replacement build.
- Packaged `quickEditEligible` metadata and preview data-update scenarios alone do not enable the product **Quick editor**. A Widget declares supported properties with `quickEdit` in `editor-assets/config.json`; a designer enables each field under **Quick Edit Settings**. Enabled text, multiline text, number, slider, toggle, color, single-file, folder, and datasource fields appear in Content/Slide and Playlist Quick editors with app-defined labels, order, and groups. `defaultEnabled` seeds only new placements.
- A newly placed Widget's non-independent datasource pickers inherit an available content base binding and selected row; independent pickers keep their own source. App file pickers retain the declared file type for single or multiple selections; Quick editor supports single-file replacements only.

## Example breadth

| Public example | Declared datasource contract |
|---|---|
| KPI Operations (`kpi-operations`) | `CUSTOM`: metrics and trend history |
| Manufacturing Andon (`manufacturing-andon`) | `TABLE`: production status |
| Airport Departures (`airport-departures`) | `TABLE`: departure records |
| Pharmacy Pickup Queue (`pharmacy-pickup-queue`) | `TABLE`: pickup queue records |

**Restaurant Menu** (internal identity **Cordo Menu Board**) is one gold reference for a photo-led brief and editable `TABLE` pattern. Catalog examples demonstrate breadth; each has its own contract and evidence, without an assumption of equal validation or production history.

## Identity, compatibility, and limits

- Widget ZIPs require `editor-assets/config.json`; its app name and version determine runtime identity, not the record name. A Widget replacement with a different identity can leave existing placements unavailable; preserve both values for a compatible replacement. A deliberately incompatible version is installed separately and does not migrate existing placements automatically.
- HTML/CSS/JS bundle ZIPs require `index.html` at the archive root. Use relative asset paths. The server generates the HTML app's App ID and version and preserves them when its bundle is replaced. Replacement keeps the app record and enabled state for either type; customer storage quota is checked against the full new extracted bundle before the old bundle is released.
- Accepted Widget-guide delivery includes modern `app.js`, legacy `app-chrome-49.js`, and `editor-assets/config.json`. ZIP upload and extraction each have 1 GiB limits; quota is a separate check.
- A source ZIP is a maintenance artifact. Browserless `_UNVERIFIED.zip` delivery is a transfer artifact, not accepted/upload-ready; complete normal delivery in a browser-capable environment.
- Widget JavaScript runs in the editor/player host with platform APIs. Display-originated datasource writes require an internal datasource explicitly editable by displays; binding data alone does not grant write access.
- A Widget can opt its first-placement, double-click, or custom-settings dialog into fullscreen for a same-origin configuration URL; a close control replaces the usual title bar, and Escape closes the dialog. In the legacy web displayer, apps that explicitly register active audio with the host can duck background audio and receive mute/duck state. The editor does not expose that audio bridge, so each app must implement it.
- HTML bundles run in a sandboxed iframe by default; **Trust bundle** opts into same-origin access, while **Allow microphone** and **Allow camera** are separate opt-ins.
- Customer-owned apps use customer-scoped catalogue and runtime resolution. Platform-app customer assignments control Designer availability; already-placed platform apps hidden by an assignment change can remain renderable. The legacy public resolver resolves platform app identities without customer context.
