I have copied this Page from Andreas Sefzigs Homepage.

This is a Homepage Page.
The content is published.
It has the title "Apps".
The summary is "How App Coder distributions integrate with the Homepage.".

The canonical Web URL is `https://homepage.sefzig-net.workers.dev/docs/apps`.
The Markdown URL is `https://homepage.sefzig-net.workers.dev/source/pages/docs/apps/Content.md`.
The Prompt URL is `https://homepage.sefzig-net.workers.dev/source/pages/docs/apps/Prompt.md`.

The Markdown:
"""
# Apps

_This page is the contract between my Apps and my Homepage._

## About this contract [v]

_App Coder and Homepage Builder collaborate on integrating the Apps._

This page describes how App Coder and Homepage Builder build, host, share, and publish the Apps.

Contract scope [<]
  The contract is generally binding for App Coder, with cleared exceptions being allowed. 

  For Homepage Builder, this is the living document explaining all App integrations.

  The [reference implementation "Maps"](#maps-reference) shows the distinction between general (required) and custom (optional) functionality.

Contract language [v]
  - **must** means required for compatibility.
  - **should** is the normal recommendation for App Coder.
  - **may** means optional behavior supported by Homepage.
  - **not available** means an App can not depend on that yet.

  Unless a section says **next design pass**, it describes current behavior.
  A **should** statement is current guidance; a **next design pass** note describes planned or proposed work.

The related API offering has [its own documentation](#tba); this page only explains how Apps consume curated Homepage APIs.

The [Toasts](https://homepage.sefzig-net.workers.dev/docs/toasts) page defines parent-owned Toast presentation and lifecycle. This page defines the App-side bridge contract for requesting Toasts and receiving Toast actions.

## Integrate an App [v]

_Requirements for Apps that want to be integrated in the Homepage._

### Build and integrate an App [v]

Use this path when creating or integrating an App:

1. Build or synchronize the current App source into `work/apps/<slug>/dist/`. The local `work/apps` symlink exposes `dist/` to Homepage Builder; it does not refresh `dist/` after a source change. Published distributions are copied files.
2. Add a valid `dist/manifest.json` with an explicit slug, entry, supported contexts, and bridge capabilities.
3. Run `make generate`. If generation fails, inspect `work/backend/reports/latest.md` and verify the App slug in the generated registry before debugging runtime behavior.
4. Open the Homepage direct route `/app/<slug>` and verify that the App works in direct mode. Standalone `dist/index.html` testing is a separate App-owned environment.
5. Add inline or full Homepage integration only when the App needs it, and declare the supported contexts in its manifest.
6. If hosted, install the message listener before rendering host-dependent UI. Validate the source, origin, expected App slug, and exact `instanceId` on instance-scoped messages. Handle `homepage/app/config`, `homepage/app/context`, `homepage/app/host-ready`, and `homepage/app/ready`; handle `homepage/app/layout` and live `homepage/app/preference` when the App declares or uses them; handle `homepage/app/toast-action` when the App offers Toast actions; and ignore unsupported messages safely. See [General bridge](#general-bridge), [Initialization handshake](#initialization-handshake), and [Host presentation timing](#host-presentation-timing) for the detailed contract.
7. Use Homepage Preferences for persistent, user-facing settings unless the App has a documented reason to keep a control inside its own UI. See [Preferences](#preferences) for the baseline.
8. Use or request a named Homepage API route for provider-backed work. Never ship provider credentials in the App.
9. Request parent-owned Toasts for relevant user-facing App events with `homepage/app/toast`; report named API failures and recovery with `homepage/app/api-status` so Homepage can maintain Preferences Warnings. Apps may report analytics events through `homepage/app/track`; keep those properties safe for CDP delivery. Keep a safe App fallback for hard failures.
10. Test direct, inline, full, reload, malformed-input, Toast, action, and unavailable-API behavior.
11. Publish the refreshed `dist/` output and verify the public App route.

The rest of this page expands each step.

#### Minimum integration choices [v]

| App need | Minimum current choice |
| --- | --- |
| Standalone App only | the standalone distribution, a working entry document, and no bridge dependency. |
| App embedded in Content | inline mode, opaque embed configuration, and a deliberate height strategy. |
| App presented as a Homepage surface | full mode, full-surface layout handling, and an instance-scoped bridge. |
| Persistent user settings | Hosted Preferences contribution for embedded instances; App-owned storage for direct-only settings. |
| Provider-backed data | A named, same-origin Homepage API route. |
| App-specific host behavior | A documented, opt-in extension rather than a new implicit convention. |

### Contract at a glance [v]

#### Capabilities and extensions [v]

These are the generally available capabilities. Each still requires the corresponding manifest and hosting context:

| General capability | Purpose | [v] General Capabilities
| --- | --- |
| Distribution and manifest | Makes the App discoverable and publishable. |
| Direct, inline, and full contexts | Defines where the App can run. |
| Embed configuration | Delivers authored App-specific input. |
| Host context | Delivers Homepage presentation context. |
| Layout context | Delivers current safe-area information to embedded instances. |
| Hosted Preferences | Lets Homepage render persistent App settings. |
| Lifecycle controls | Lets an embedded App expose Start/Pause and Restart when that model fits. |
| Smart height | Lets an inline App request a host-managed height. |
| Curated Homepage APIs | Gives the App selected server-side capabilities without exposing provider credentials. |
| API status reporting | Lets a hosted App report safe API failure and recovery state for the Preferences Warnings surface. |

Anything outside that set is an extension:

| Extension rule | Meaning | [v] Extension Rules
| --- | --- |
| It is explicit | The manifest, Embed configuration, or App-specific requirement declares it. |
| It is scoped | The behavior identifies the App and, for embedded behavior, the exact instanceId. |
| It is namespaced | Messages and API operations do not pretend to be general capabilities. |
| It is optional to other Apps | An App must not need another App's extension to implement the general contract. |
| It is documented beside the App | Maps route navigation belongs to the Maps reference and requirements, not to every App's baseline. |

#### Ownership model [v]

| Concern | App Coder owns | Homepage Builder owns | [v] Concerns
| --- | --- | --- |
| App UI and runtime | Rendering, interaction, state, and App-specific behavior. | iframe placement and Homepage Chrome around the App. |
| Distribution | Source, build output, assets, manifest, and App tests. | Discovery, validation, route serving, and publication packaging. |
| Embed configuration | The meaning of App-specific keys. | Forwarding host keys and preserving remaining configuration as config. |
| Preferences | Contribution definition and applying received values. | Preference rendering, persistence, disclosure, and delivery of changes. |
| Host context | Applying presentation and layout context. | Supplying current theme, brightness, displayed accent, CSS variables, and safe area. |
| Provider data | Requesting a documented capability and handling its result. | Owning curated server routes, provider details, and server-side credentials. |
| API status reporting | Classifying failures and recovery, then sending safe stable reports for the active instance. | Validating reports and maintaining the corresponding transient Preferences Warning. |
| Toasts and Toast actions | Choosing when feedback is relevant, supplying safe text and stable IDs, and handling accepted actions. | Validating requests, rendering and timing Toasts, grouping them, maintaining Warnings, and delivering actions. |
| Lifecycle | Reporting state and responding to accepted commands. | Showing controls and sending commands for the registered instance. |

#### Failure triage [v]

Debug from the earliest broken boundary rather than changing the App by default:

| First broken boundary | Likely owner | First check |
| --- | --- | --- |
| Authored value is missing or altered before the bridge | Content parser or generator | Inspect the authored Element, generated configuration, and parser rules. |
| App is missing from the registry or route | Homepage distribution discovery or generator | Verify `dist/manifest.json`, the App slug, generated registry, and generated assets. |
| A bridge message is absent or rejected | Homepage bridge or App listener | Validate source, origin, slug, instanceId, and message fields on both sides. |
| The App receives valid configuration or context but renders incorrectly | App source or distribution | Compare the received values with the App's rendering and state handling. |
| A named Homepage API route cannot resolve provider data | Homepage API route or provider boundary | Check the API envelope, timeout, fallback, and server-side provider requirements before changing App rendering. |

The boundary that produced the bad result owns the fix. An App must still present a safe fallback when a Homepage API or provider fails.

#### Host presentation timing [v]

For hosted Apps, Homepage owns the timeline for presentation values that it animates. See [Inbound: host context](#inbound-host-context) for the field reference.

- `accent` is the accent color currently displayed by Homepage. `accentText`, `onAccent`, and `onAccentColor` are readable foreground colors for content placed on an accent-colored surface. They are not interchangeable.
- `accentTarget` and `accentTargetText` are optional destination metadata. Embedded Apps must not use them for visible rendering.
- The initial URL `hostContext` and live `homepage/app/context` messages use the same displayed-value semantics.
- Apply incoming hosted values directly. Do not run a second CSS transition, timer, observer, or interpolation loop toward a target value. Homepage may send frequent updates while a transition is running; the latest displayed value wins.
- Bootstrap styling is applied without animation. The live bridge message remains authoritative after startup.
- Semantic fields take precedence over target-like CSS variables such as `--accent-color-new`.
- Homepage owns the outer background of an embedded surface. An embedded App must not paint its standalone fallback background over it. An App-owned canvas or renderer must use the same host-provided background semantics.
- Standalone Apps may use their own fallback colors and transitions.

#### What is not available [v]

- Apps do not receive raw Homepage API keys or provider secrets.
- Apps cannot directly read Homepage's HTTP-only settings cookie.
- There is no generic provider proxy.
- There is no general bridge version handshake or capability-discovery message.
- The manifest does not grant permission to create arbitrary API routes.
- The current host does not yet deliver every parent-context field declared by an App manifest.

### Distribution and manifest [v]

#### Source and dist [v]

App Coder may keep source, tests, requirements, documents, and logs beside the distribution. Homepage consumes only the publishable distribution:

```text [<] App structure
/work/apps/<slug>/
├── app/            App Coder source
├── dist/           Homepage-ready output
│   ├── manifest.json
│   ├── index.html
│   └── runtime assets
├── test/           App tests
├── requirements/   App-specific requirements
├── documents/      App-specific design material
└── logs/           App change history
```

The exact App Coder workspace may differ, but the boundary does not: dist is what Homepage discovers, copies, serves, and publishes. Homepage Builder does not create the App source or repair App behavior that belongs in App Coder.

#### Minimum recommended manifest [v]

This is a hosted-capable example, not a requirement that every App use every field:

```json [< nowrap] Minimum recommended manifest
{
  "slug": "example",
  "title": "Example",
  "version": "1.0.0",
  "description": "A short description of the App.",
  "entry": "index.html",
  "distribution": {
    "contexts": ["direct", "inline"],
    "preferredPresentation": "inline",
    "hostBridge": true,
    "parentContext": {
      "theme": true,
      "brightness": true,
      "accent": true,
      "accentText": true
    },
    "preferencesContributions": {
      "supported": false,
      "contexts": ["inline"]
    },
    "bridgeMessages": {
      "apiStatus": "homepage/app/api-status",
      "supportLinks": "homepage/app/support-links"
    },
    "smartHeight": true,
    "requiredAssets": [
      "index.html",
      "js/index.js",
      "css/index.css"
    ]
  },
  "supportLinks": [
    {
      "label": "Readme",
      "href": "README.md",
      "presentation": "preferences-footer"
    }
  ],
  "runtime": {
    "config": true,
    "secrets": false,
    "requiresServer": false,
    "publicOrigin": ["homepageOrigin"]
  },
  "resources": {
    "apiRoutes": []
  }
}
```

runtime and resources are declarations in the distribution manifest. They do not dynamically create permissions or routes.

#### Manifest fields [v]

| Field | Current contract |
| --- | --- |
| slug | Must match the App directory name and intended public route. |
| entry | Must be a relative file inside dist. The current generator defaults to index.html when omitted, but an App should declare it explicitly. |
| title, shortTitle, version, description | App metadata. Missing display fields receive generator fallbacks. |
| distribution.contexts | Authoritatively declares the supported Homepage contexts. It must list one or more of `direct`, `inline`, and `full`; an undeclared context is rejected. A direct App must also declare `full` because the direct route uses the full hosted surface. |
| distribution.preferredPresentation | Presentation hint; it does not force a hosting mode. |
| distribution.hostBridge | Must be `true` when the App declares a Homepage context. Hosted contexts require the bridge. |
| distribution.parentContext | Declares the host-context fields the App expects. The current general contract supports theme, brightness, and accent fields; do not declare or depend on `reducedMotion`, `textScale`, or `devMode` yet because Homepage does not deliver them through the generic host context. |
| distribution.preferencesContributions | Seeds the hosted Preferences contribution for an embedded or direct full-context instance. |
| distribution.bridgeMessages | Describes the namespaced bridge messages the App uses. An API-reporting App declares `apiStatus: homepage/app/api-status`; Toast requests are globally available and need no capability declaration. |
| distribution.smartHeight | Enables accepted homepage/app/resize requests for inline instances without authored height. |
| distribution.requiredAssets | Declares relative files that must exist inside dist. Generation fails when a declared asset is missing or escapes dist. |
| distribution.unsupportedContexts | Descriptive context exclusions. |
| supportLinks | Optional App links rendered by Homepage. Each link may set `presentation` to `preferences-footer` or `main-footer`; omitted presentation defaults to `preferences-footer`. Relative links resolve under `/app/<slug>/`, while root-relative and absolute links are kept as authored. |
| runtime.config | Declares that the App has non-secret runtime configuration. |
| runtime.secrets | Must be false for browser-published App output. |
| runtime.requiresServer | Declares that the App needs a server-backed runtime or API. |
| runtime.publicOrigin | Declares the public-origin configuration the App expects. |
| resources.apiRoutes | Documents named App-related API resources. It does not register a route or create a proxy. |
| top-level id | Inventory metadata in the current Maps manifest; it is not a public URL. Homepage derives its registry identity from the slug. |

#### Distribution validation [v]

Homepage currently:

- discovers dist/manifest.json;
- requires valid JSON;
- requires the manifest slug to match the App directory;
- requires an explicit, non-empty list of supported contexts and rejects unknown or duplicate context names;
- requires bridge support for every declared Homepage context;
- requires `full` when `direct` is declared;
- rejects an absolute or escaping entry path;
- requires the entry file to exist;
- exposes the direct route only when `direct` is declared;
- serves embedded assets only for their declared `inline` or `full` context;
- verifies every declared required asset inside dist;
- inventories files below dist for public App assets.

An invalid manifest, missing entry, unsupported context, missing bridge declaration, or missing required asset prevents that App from becoming a valid registry entry. Do not place API keys, provider secrets, login tokens, or other credentials in dist.

> **Current boundary:** Context support, bridge participation, and required assets are enforced. Parent-context fields, bridge-message declarations, and API/resource declarations remain descriptive; they do not grant permissions or create routes.

#### API registrations and capabilities [v]

resources.apiRoutes is the registration source for an App's named Homepage API dependencies. It remains declarative: the Backend preserves the metadata in the generated App registry, while each actual Homepage route is explicitly implemented and registered by Homepage.

Credentialed host-provided routes must declare a stable capability and logical credential identifier:

```json [< nowrap] Registered API resource
{
  "id": "image-generator",
  "capabilityId": "api:image-generator",
  "method": "POST",
  "path": "/api/image-generator",
  "hostProvided": true,
  "required": true,
  "requiresCredentials": true,
  "credentialId": "openai-image"
}
```

capabilityId is the semantic contract consumed by the App handshake. id identifies the technical resource; it must not be used as permission to create a route. required is separate from requiresCredentials: a credentialed resource may be optional in a future App. The current Image Generator pilot resolves openai-image to the global APIs Preferences group and never exposes the key itself.

### Hosting and Embeds [v]

#### Contexts [v]

| Context | Entry behavior | Instance behavior |
| --- | --- | --- |
| direct | Homepage route `/app/<slug>` opens the App as a direct Homepage route in a Homepage-managed full-surface iframe. | Receives the full host contract, including a route-scoped instanceId, config, host context, layout, and any contributed Preferences supported by the route. It is not an authored Embed Element. |
| inline | Homepage serves the entry with ?embed=inline inside an iframe. | Receives an instanceId, config, hosted Preferences when contributed, layout information, and optional smart-height handling. |
| full | Homepage serves the entry with ?embed=full inside an iframe. | Receives the full hosted contract and remains surrounded by Homepage Chrome. |

direct and full are not synonyms. direct describes how the App is reached as a route. From the App's perspective, the direct route currently uses the full hosting context; full also describes an authored full Embed.

Homepage has three App entry contexts: direct, inline, and full. Direct is a Homepage route; inline and full are authored Embed modes. A standalone `dist/index.html` served outside Homepage is a development or diagnostic context, not a fourth Homepage mode. Do not treat `/app/<slug>` and the standalone distribution entry as interchangeable.

#### Local test contexts [v]

The local URL identifies who owns the surrounding runtime:

| Test context | URL shape | Ownership |
| --- | --- | --- |
| Codex Homepage preview | `http://localhost:5174/...` | Homepage serves the route and owns any Embed or direct-App hosting. |
| Owner Homepage preview | `http://localhost:5173/...` | Homepage serves the route in the Owner preview environment. |
| Standalone App preview | An App-specific HTTP server URL ending in the distribution entry, usually `index.html`. | The App owns the page; Homepage does not send bridge messages. The port is App-specific. |
| `file://` diagnostic | A local file URL only when the App explicitly supports it. | Not a supported Homepage integration context; origin, iframe, and renderer behavior may differ. |

When testing in the Codex or in-app browser environment, use port 5174. Do not infer Homepage hosting from the standalone App URL.

#### Authored Embed [v]

The authored Element identifies the App and supplies App-specific input:

```text [<] Embed configuration
element embed
app: maps
name: Berlin
mode: inline
height: 420
address: Berlin, Germany
```

An Orgs Embed uses the same required `name` key for its Preferences heading and lifecycle Toasts:

```text [<] Orgs Embed configuration
element embed
app: orgs
name: Agents
mode: inline
height: 420
fullscreen: Hover
minimap: Hover
zoom: Fit
```

Homepage interprets these host keys:

| Key | Current behavior |
| --- | --- |
| app or slug | Identifies the registered App. app is the normal authored form. |
| mode | Exact full selects the full hosting mode; every other accepted Embed defaults to inline. |
| screen | Enables the responsive screen presentation only for a full Embed when true. |
| height | Controls inline host height and is not forwarded to the App's config. |
| name (Maps and Orgs) | Required for authored Maps and Orgs Embeds, in either inline or full mode. Maps uses it for the Preferences section (`Map: <name>`), forwards it to Maps, and shows it beside the existing loading bar while an embedded map loads. For Orgs, it is the canonical organization reference (matched against the org's id, slug, or display name); Homepage trims and forwards it as `config.name` and uses that same value for the Content Preferences section (`Orgs: <name>`) and exact hosted Toast text `Org "<name>" entered` / `Org "<name>" exited`. A terminal loadability warning uses `Org "<name>" unavailable`. |
| org (Orgs only) | Not supported. `name` is the sole organization reference; no fallback or compatibility handling is provided for `org`. |
| Every other key | Is forwarded as App-specific `config` after Content parsing and is interpreted by the App. |

#### Content-to-App data path [v]

For an authored Embed, configuration reaches the App in this order:

```text [<] Content-to-App path
Content.md authored Embed
        │
        ▼
Element parser
        │  comments are removed and YAML values are parsed
        ▼
Generated presentation configuration
        │
        ▼
Homepage host-key removal
        │  app, slug, mode, screen, and height are used by Homepage
        ▼
homepage/app/config
        │
        ▼
App listener receives App-specific config
```

In Element YAML, an unquoted `//` starts a comment and the rest of that line is removed. Quote URLs and other values containing `//` when they are meant to reach the App. Quoted `//` is preserved.

Generated presentation files are outputs of `make generate`. Inspect them when debugging the path, but do not edit them by hand; fix the authored Content, generator, or App distribution that produced them.

Homepage generates the instanceId for an embedded occurrence. Treat it as opaque: do not parse its path, derive a durable identifier from its format, or use another instance's value.

#### Height and full Slides [v]

For an inline App:

- an authored numeric height wins;
- an authored percentage becomes a viewport-relative height;
- an authored ratio becomes an aspect ratio;
- without an authored height, the host uses its fallback height;
- with smartHeight: true, the App may request a finite height through homepage/app/resize;
- accepted smart height is currently clamped to 360px through 560px.

A full App does not use inline height handling. A full App Slide contains exactly one full App Embed among its body elements. screen: true opts that full App into the responsive screen presentation.

The direct `/app/<slug>` route uses the same full-context surface without ordinary Content padding: its iframe fills the available route-frame width and viewport height, while the route frame continues to reserve the Preferences area and Homepage Chrome remains available as an overlay. This direct-host rule does not change inline App embeds or the dedicated direct Maps surface.

### Runtime lifecycle [v]

#### Hosted startup sequence [v]

The current hosted sequence is:

The direct route follows the full branch below: Homepage owns the route and creates the managed full-surface frame without an authored Embed Element.

```text [<] Hosted App lifecycle
Homepage chooses the inline or full embed mode
        │
        ├── serves /app/<slug>/<entry>?embed=<embedMode>
        ├── provides hostContext when pre-paint context is available
        ▼
App document starts
        │
        ├── optional bootstrap applies initial host styling
        ├── App installs its message listener
        ▼
Homepage sends
        │
        ├── homepage/app/context
        ├── homepage/app/config
        ├── homepage/app/layout when relevant
        └── homepage/app/host-ready
        ▼
App accepts the initial contract
        │
        └── homepage/app/ready
        ▼
Homepage replays the latest config, context, and layout
        │
        ├── App prepares and renders
        ├── optional homepage/app/preferences
        ├── optional homepage/app/support-links
        ├── optional homepage/app/controls
        └── optional resize or App-specific messages
        ▼
Homepage sends live Preference, layout, and lifecycle changes
        │
        └── removing or reloading the iframe ends the instance
```

#### Initialization handshake [v]

The handshake confirms that both sides have initialized the same embedded instance:

1. After iframe load, Homepage sends the initial `config`, `context`, and relevant `layout`, then `homepage/app/host-ready`.
2. The App sends `homepage/app/ready` only after it has accepted `config` and `host-ready` for the same `slug`, `embedMode`, and `instanceId`.
3. Homepage validates `ready` and replays the latest `config`, `context`, and `layout`. Replayed snapshots must be safe and idempotent; they do not create a new instance.
4. Reload, replacement, or unmount resets the handshake. A reload must complete it again, even when the route keeps the same `instanceId`.
5. Homepage waits four seconds for diagnostics only. A timeout must not block rendering, and a valid late `ready` is still accepted.

Standalone Apps ignore both handshake messages. An App must install its listener early and tolerate the initial snapshots arriving before or after its visual runtime is ready.

#### Pre-paint host context [v]

For a hosted entry document, Homepage can place a base64url-encoded hostContext query parameter on the App URL. When the value is valid, the App asset route injects a small script into index.html before the first stylesheet:

- window.HOMEPAGE_HOST_CONTEXT contains the initial style context;
- document.documentElement.dataset.hosted is set to homepage;
- initial theme, brightness, accent, and CSS variables can be applied before first paint.

An App that needs to avoid a flash of the wrong theme should apply this bootstrap context before rendering, without animating to it. The live homepage/app/context message remains authoritative after load. The bootstrap is a host-provided optimization, not a substitute for the bridge listener. See [Host presentation timing](#host-presentation-timing).

The Maps distribution uses js/host-context-bootstrap.js for this purpose. A future generalized bootstrap package is not yet a separate platform contract.

#### Direct and embedded startup [v]

| Startup | App behavior |
| --- | --- |
| Direct | `/app/<slug>` is served by Homepage as a managed full-surface iframe without an authored Embed Element. The App receives the full bridge contract; direct-route-specific controls or storage remain App-owned. |
| Embedded | An authored Embed uses inline or full mode. The App should use config, Preferences, host context, and bridge updates rather than depending on Homepage DOM or storage. |

For hosted Maps, the App waits for homepage/app/config before preparing authored map content. This is a good general pattern when an App cannot render meaningful content without authored configuration.

#### Repeat loads and teardown [v]

- Treat repeated equivalent homepage/app/config messages as idempotent.
- Replace App-local state only when configuration or Preference values genuinely change.
- Do not assume that an iframe reload creates a new authored occurrence; always use the current instanceId.
- There is no general teardown message. Stop listeners, timers, observers, and network work when the document is unloaded or the App runtime is otherwise disposed.

## Integrate the Homepage [v]

_Use the capabilities the embedding Homepage provides to an App._

### General bridge [v]

#### Protocol and trust [v]

The bridge uses window.postMessage. Hosted Apps currently run at the same origin as Homepage. The App therefore uses its own current origin as the expected Homepage origin; it does not learn a trusted origin from the parent.

For messages received from Homepage, an App should:

- require event.source === window.parent;
- compare event.origin with `window.location.origin`;
- require an object message and recognized type;
- require the expected App slug when present;
- require the exact instanceId for instance-scoped capabilities.

For messages sent to Homepage, use `window.parent.postMessage` with `window.location.origin`. Do not use an origin supplied by the parent, `document.referrer`, or the first incoming message. A wildcard target is not an accepted contract. Cross-origin hosting would require a separate explicit allowlist or handshake design.

Homepage validates frame source, origin, App path, slug, and, where applicable, instance identity. Unknown or malformed messages are ignored; there is no generic error response.

#### Message shape and vocabulary [v]

Every bridge message is a flat object. The shared fields are:

| Field | Meaning |
| --- | --- |
| type | The exact namespaced message name. |
| slug | The registered App slug. |
| instanceId | The opaque identity of one embedded App occurrence, when the message is instance-scoped. |
| Other fields | Message-specific data kept at the top level. There is no generic `payload` object. |

`homepage/app/context` is the **host-context message**. The `embedMode` field in `homepage/app/config` and `homepage/app/layout` is the **embed mode**: `inline` or `full`. Do not use `context` for the embed mode.

Use these terms consistently:

| Term | Meaning | Examples |
| --- | --- | --- |
| Snapshot | Homepage sends current data to the App. | `config`, host `context`, `layout` |
| Update | Homepage sends a changed value. | `preference` |
| Command | Homepage asks the App to do something. | `start`, `pause`, `restart`, Maps `navigation`, `toast-action` |
| Request | The App asks Homepage to do something. | `resize`, `toast` |
| Contribution | The App registers capabilities or controls. | `preferences`, `controls` |
| Report | The App reports an event or current state. | `api-status`, `route-painted`, `route-failed`, `navigation-state` |

Use **state** only for a current condition, **event** for something that happened, **command** for an instruction, and **request** for an App-to-Homepage ask.

#### Minimal listener pattern [v]

This is the smallest useful hosted listener pattern. The helper functions are App-owned; the checks and message names are the current host contract.

`js [v nowrap] Generic bridge listener
const hostOrigin = window.location.origin;
const appSlug = "example";
let instanceId = null;

function trustedHostMessage(event) {
  return window.parent !== window
    && event.source === window.parent
    && event.origin === hostOrigin
    && event.data && typeof event.data === "object";
}

function onHostMessage(event) {
  if (!trustedHostMessage(event)) return;
  const message = event.data;
  if (message.slug && message.slug !== appSlug) return;

  if (message.type === "homepage/app/config") {
    if (message.embedMode !== "inline" && message.embedMode !== "full") return;
    instanceId = message.instanceId;
    applyConfig(message.config || {}, message.preferences || {});
  } else if (message.type === "homepage/app/context") {
    applyHostContext(message);
  } else if (
    message.type === "homepage/app/preference"
    && message.instanceId === instanceId
    && message.source === "host-preferences"
  ) {
    applyPreference(message.preferenceId, message.value);
  }
}

window.addEventListener("message", onHostMessage);
window.addEventListener("pagehide", () => {
  window.removeEventListener("message", onHostMessage);
});
`

Treat this as a pattern, not a platform helper. Add handling for layout, lifecycle, and App-specific messages only when the App declares and needs them.

#### Message index [v]

The table is the compact parameter reference. The sections below provide semantics and examples.
Rows whose message names include `/maps/` are Maps extension examples, not requirements for every App.

| Direction | Message | Required / accepted fields | Scope and constraints |
| --- | --- | --- | --- |
| Homepage → App | homepage/app/config | type, slug, instanceId, embedMode, config, preferences | Snapshot for an embedded instance; embedMode is inline or full. |
| Homepage → App | homepage/app/context | type, slug, theme, brightness, brightnessPreference, accent, onAccent, onAccentColor, accentText, accentTarget, accentTargetText, accentRevision, cssVars | Snapshot/update of live presentation context; `accent` is the currently displayed value and target fields are optional destination metadata. |
| Homepage → App | homepage/app/layout | type, slug, instanceId, embedMode, safeArea | Snapshot of embedded layout; embedMode is inline or full and safeArea contains numeric insets. |
| Homepage → App | homepage/app/host-ready | type, slug, instanceId, embedMode | Host side of the initialization handshake; follows the initial snapshots for this instance. |
| Homepage → App | homepage/app/preference | type, slug, instanceId, preferenceId, value, source | Update for an embedded instance; source is host-preferences. |
| Homepage → App | homepage/app/start, homepage/app/pause, homepage/app/restart | type, slug, instanceId | Command for an embedded instance; accepted only after matching lifecycle registration. |
| Homepage → App | homepage/app/toast-action | type, slug, instanceId, actionId, issueId optional | Command for the exact App instance after a user invokes its Toast action. |
| Homepage → App | homepage/app/maps/navigation | type, slug, instanceId, command | Maps command; command is step, location, leg, or overview. |
| App → Homepage | homepage/app/resize | type, slug, instanceId, height | Request from an inline App; accepted only with smartHeight and no authored height. |
| App → Homepage | homepage/app/ready | type, slug, instanceId, embedMode | App side of the initialization handshake; sent once after the matching config and host-ready messages are accepted. |
| App → Homepage | homepage/app/preferences | type, slug, instanceId, sections; version optional | Preferences contribution; a new accepted contribution replaces the previous one. |
| App → Homepage | homepage/app/image-generator-preferences-sync | type, slug, instanceId, embedMode, source, values | Image Generator template-selection update for declared Output values; accepted only from the expected exact instance and only when each value is declared by the hosted Output section. It updates host-owned saved values without replacing the contribution or using the manual preference-change path. |
| App → Homepage | homepage/app/support-links | type, slug, instanceId, links | Footer-link contribution; each link has `label`, `href`, and optional `presentation`; a new accepted contribution replaces the previous links for the App instance, and an empty list removes them. |
| App → Homepage | homepage/app/preferences-request | type, slug, instanceId, embedMode, action, sectionId, focusSection, collapseOtherSections | Image Generator request to open and focus the declared `image-generation` Preferences section; accepted only from the expected App instance. |
| App → Homepage | homepage/app/controls | type, slug, instanceId, controls.startPause, controls.restart, state | Lifecycle contribution; state is ready, running, or paused. |
| App → Homepage | homepage/app/toast | type, slug, instanceId, severity, message/title/duration/persistent/dedupeKey/issueId/issueState/action optional | Parent-owned user-facing notice; warning/error requests use an active or resolved issue identity. |
| App → Homepage | homepage/app/track | type, slug, instanceId, eventName, properties optional | Analytics event request; accepted only from the expected App instance and relayed through Homepage's dataLayer to MTAM. Event names are at most 100 characters; JSON properties are at most 16 KiB. |
| App → Homepage | homepage/app/api-status | type, slug, instanceId, operation, state, code/message/httpStatus/details optional | Named API failure or recovery report; accepted only from the expected App instance and transformed into a parent-owned Preferences Warning. |
| App → Homepage | homepage/app/maps/route-painted | type, slug, instanceId | Maps event report; emitted after a route is visibly prepared. |
| App → Homepage | homepage/app/maps/route-failed | type, slug, instanceId, diagnostic optional | Maps event report; diagnostic must remain safe for the user. |
| App → Homepage | homepage/app/maps/navigation-state | type, slug, instanceId, navigationMode, position, locationIndex, legIndex, locationCount, legCount, canGoPrevious, canGoNext | Maps state report for the prepared route position. |

#### Outbound: CDP track event [v]

Apps may report an event that has occurred using the flat `homepage/app/track` message. This is optional for each App; it does not add an App-specific CDP client or bypass MTAM.

```js
window.parent.postMessage({
  type: "homepage/app/track",
  slug: "image-generator",
  instanceId,
  eventName: "Generated Image",
  properties: {}
}, window.location.origin);
```

The App uses its current `instanceId` and `window.location.origin`. Homepage accepts the request only from the expected iframe, origin, App slug, and exact active instance. It validates the event name (non-empty, no control characters, at most 100 characters) and optional JSON-object properties (at most 16 KiB when serialized), then writes a `cdpEventType: "track"` event to `window.dataLayer`. Homepage assigns `appSlug` from the validated App identity; that property is reserved and any App-provided value is discarded.

MTAM's CDP agent alone calls `analytics.track(eventName, { ...properties, appSlug })`, after the initial Homepage `identify()`. Events received while Analytics.js is loading are queued. Do not load or call the CDP SDK from an App. Track properties must not contain credentials, secrets, generated image bytes, private prompts, or other sensitive request data.

#### Inbound: embed configuration [v]

homepage/app/config is sent to embedded instances. It carries the host identity, authored configuration, and saved Preference values:

```json [v nowrap] Example message
{
  "type": "homepage/app/config",
  "slug": "maps",
  "instanceId": "/home#map-1",
  "embedMode": "inline",
  "config": {
    "address": "Berlin, Germany"
  },
  "preferences": {
    "zoom-adjustment": 0.2
  },
  "capabilities": {
    "api:image-generator": {
      "available": false,
      "state": "missing",
      "credentialId": "openai-image",
      "setupTarget": {
        "groupId": "apis",
        "credentialId": "openai-image"
      }
    }
  }
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| type | Yes | Exact message name. |
| slug | Yes | Registered App slug. |
| instanceId | Yes for embedded delivery | Opaque identity of this occurrence. |
| embedMode | Yes | inline or full. |
| config | Yes | Authored App-specific keys after host keys have been removed. |
| preferences | Yes | Saved host Preference map; may be empty. |
| capabilities | No for Apps without registered capabilities | Non-sensitive, server-derived capability status. state currently supports available, missing, invalid, and unavailable; available means configured, not provider-validated. |
| credentialAvailable | Legacy fallback only | Boolean compatibility field for the Image Generator pilot. Apps should prefer capabilities. |

Homepage removes app, slug, mode, screen, and height from authored input before putting the remaining keys into config. Capability status is recomputed when the App loads and after relevant Homepage Preferences changes. Homepage never sends credentials or raw keys.

#### Inbound: host context [v]

homepage/app/context is sent at initialization and when relevant Homepage presentation context changes:

```json [v nowrap] Example message
{
  "type": "homepage/app/context",
  "slug": "maps",
  "theme": "system",
  "brightness": "dark",
  "brightnessPreference": "dark",
  "accent": "#246bfe",
  "onAccent": "#ffffff",
  "onAccentColor": "#ffffff",
  "accentText": "#ffffff",
  "accentTarget": "#246bfe",
  "accentTargetText": "#ffffff",
  "accentRevision": 3,
  "cssVars": {
    "--accent-color": "#246bfe",
    "--color-back": "#101216",
    "--color-surface": "#171a21"
  }
}
```

Current semantic fields are theme, brightness, brightnessPreference, accent, onAccent, onAccentColor, accentText, optional accentTarget and accentTargetText, accentRevision, and cssVars. `accent` and its text color are the values currently displayed by Homepage, including intermediate values while an accent transition is running. Embedded Apps must use those fields for visible styling and must not replace them with the optional target values. Homepage may send context updates for each animation frame during the transition. See [Host presentation timing](#host-presentation-timing) for the ownership rule.

The current allowlisted CSS variable set includes:

```text [v] Current host CSS variables
--accent-color
--accent-color-text
--accent-color-text-surface
--accent-color-text-panel
--accent-color-light
--accent-color-dark
--accent-color-enhanced-light
--accent-color-enhanced-dark
--accent-color-standard
--accent-color-enhanced
--accent-color-standard-surface
--accent-color-enhanced-surface
--accent-color-standard-panel
--accent-color-enhanced-panel
--accent-color-standard-light-background
--accent-color-standard-light-surface
--accent-color-standard-light-panel
--accent-color-standard-dark-background
--accent-color-standard-dark-surface
--accent-color-standard-dark-panel
--accent-color-enhanced-light-background
--accent-color-enhanced-light-surface
--accent-color-enhanced-light-panel
--accent-color-enhanced-dark-background
--accent-color-enhanced-dark-surface
--accent-color-enhanced-dark-panel
--on-accent-color
--accent-color-prior
--accent-color-new
--accent-border-prior-color
--accent-border-new-color
--on-accent-color-prior
--on-accent-color-new
--nav-primary-icon-color
--nav-secondary-icon-color
--chrome-accent-shade-color
--chrome-accent-shade-strength
--color-main
--color-muted
--color-back
--color-surface
--color-panel
--color-border
--color-border-strong
--color-control
--color-control-secondary
--color-control-disabled
--color-code-content-back
```

The App should use semantic fields first and treat cssVars as host styling context. The current generic host message does not include `reducedMotion`, `textScale`, or `devMode`. Current App distributions must not declare or depend on those fields; adding one requires a separately coordinated host-and-App contract change.

#### Inbound: layout context [v]

homepage/app/layout is sent to an embedded instance when the usable layout or safe area changes:

```json [v nowrap] Example message
{
  "type": "homepage/app/layout",
  "slug": "maps",
  "instanceId": "/home#map-1",
  "embedMode": "inline",
  "safeArea": {
    "top": 0,
    "right": 0,
    "bottom": 72,
    "left": 0
  }
}
```

safeArea contains numeric CSS-pixel insets. A direct App does not receive an instance-scoped layout message.

#### Inbound: Preference change [v]

homepage/app/preference is sent after a hosted Preference is committed:

```json [v nowrap] Example message
{
  "type": "homepage/app/preference",
  "slug": "maps",
  "instanceId": "/home#map-1",
  "preferenceId": "zoom-adjustment",
  "value": 0.5,
  "source": "host-preferences"
}
```

value is currently a string, number, or boolean. The App should update its runtime when the Preference is intended to be live. The current host emits changes according to each control's commit behavior; text currently commits on blur.

#### Inbound: lifecycle commands [v]

Homepage may send these commands after an App has registered lifecycle controls:

```json [v nowrap] Commands
{
  "type": "homepage/app/start",
  "slug": "maps",
  "instanceId": "/home#map-1"
}
{
  "type": "homepage/app/pause",
  "slug": "maps",
  "instanceId": "/home#map-1"
}
{
  "type": "homepage/app/restart",
  "slug": "maps",
  "instanceId": "/home#map-1"
}
```

Commands are instance-scoped. The App must keep its reported state accurate and must not expose a control it cannot honor.

#### Outbound: smart height [v]

An inline App with smartHeight true may send:

```json [v nowrap] Example message
{
  "type": "homepage/app/resize",
  "slug": "maps",
  "instanceId": "/home#map-1",
  "height": 480
}
```

Homepage accepts the request only for an inline instance with no authored height, validates the exact instanceId, and clamps the result to the current 360px–560px range.

#### Outbound: hosted Preferences [v]

An App may contribute sections and controls through homepage/app/preferences. The manifest can provide the initial contribution, and the App can send a runtime contribution when its available controls change.

```json [v nowrap] Example message
{
  "type": "homepage/app/preferences",
  "slug": "maps",
  "instanceId": "/home#map-1",
  "version": "1.7.0",
  "sections": [
    {
      "id": "map-behavior",
      "label": "Map",
      "initialOpen": false,
      "preferences": [
        {
          "id": "zoom-adjustment",
          "label": "Zoom",
          "control": "range",
          "min": -3,
          "max": 3,
          "step": 0.1,
          "defaultValue": 0,
          "liveUpdate": true,
          "persistence": "homepage"
        }
      ]
    }
  ]
}
```

Homepage binds an accepted runtime contribution to the current frame's instance. Invalid sections or incomplete Preference records are dropped. A later accepted contribution for the same App and instance replaces the earlier contribution.

#### Outbound: Image Generator template-output synchronization [v]

The Image Generator uses a separate message when a hosted template changes its Output defaults. This is not a replacement Preferences contribution and does not represent a manual Homepage control change.

```json [v nowrap] Example message
{
  "type": "homepage/app/image-generator-preferences-sync",
  "slug": "image-generator",
  "instanceId": "/home#image-generator-1",
  "embedMode": "full",
  "source": "template-selection",
  "values": {
    "model": "fast",
    "format": "png",
    "background": "transparent",
    "ratio": "9:16"
  }
}
```

Homepage accepts this message only from the expected Image Generator frame and exact active instance. It validates the mode, source, known Output ids, allowlisted values, and the active `image-generation` section's Homepage-persisted `select` declarations. `quality` is accepted only when that hosted section declares it. Accepted values update the saved instance snapshot and the visible controls together; invalid or stale messages are ignored and ordinary contributions and manual preference changes keep their existing behavior.

#### Outbound: App footer links [v]

An App may seed footer links in the manifest's top-level `supportLinks` array and may replace them at runtime with `homepage/app/support-links`. Homepage accepts the runtime contribution only from the expected App frame, origin, slug, and exact `instanceId`.

```json [v nowrap] Example message
{
  "type": "homepage/app/support-links",
  "slug": "example",
  "instanceId": "/home#example-1",
  "links": [
    {
      "label": "Guide",
      "href": "README.md",
      "presentation": "main-footer"
    },
    {
      "label": "Settings",
      "href": "/docs/apps",
      "presentation": "preferences-footer"
    }
  ]
}
```

`presentation: "main-footer"` renders in the main Homepage footer. `presentation: "preferences-footer"`, or an omitted presentation, renders in the Preferences footer. Relative links resolve to the App route; root-relative and absolute links remain unchanged. A later accepted contribution replaces all footer links for that App instance, and sending `links: []` clears them.

#### Outbound: lifecycle registration [v]

An embedded App registers the controls it wants Homepage to show:

```json [v nowrap] Example message
{
  "type": "homepage/app/controls",
  "slug": "maps",
  "instanceId": "/presentation/road-trip#map-1",
  "controls": {
    "startPause": true,
    "restart": true
  },
  "state": "running"
}
```

state is ready, running, or paused. At least one of startPause and restart must be true. Homepage accepts the registration only for the exact App and embedded instance.

#### Outbound: API status [v]

An App reports a named API operation when it fails or recovers. The message is flat and instance-scoped:

```json [v nowrap] Example failure report
{
  "type": "homepage/app/api-status",
  "slug": "example",
  "instanceId": "/home#example-1",
  "operation": "forecast",
  "state": "failed",
  "code": "provider_unavailable",
  "message": "The forecast service is temporarily unavailable.",
  "httpStatus": 503,
  "details": [
    { "label": "Provider detail", "value": "Try again later." }
  ]
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| type | Yes | Exact message name `homepage/app/api-status`. |
| slug | Yes | Registered App slug. |
| instanceId | Yes | Opaque identity of the embedded App occurrence. |
| operation | Yes | Stable App-defined operation name, such as `forecast` or `route`. |
| state | Yes | `failed` or `resolved`. A resolved report clears the matching Warning. |
| code | No | Stable, safe App/API error code. |
| message | No for recovery | Safe user-facing explanation; Homepage supplies a generic fallback when a failure omits it. |
| httpStatus | No | Numeric HTTP status when useful; do not include raw response bodies. |
| details | No | A bounded list of safe `{ label, value }` diagnostics. Do not include credentials, authored secrets, request data, provider URLs, or raw upstream responses. |

Apps should report `failed` for a network failure, non-2xx response, unreadable response, or API envelope with `ok: false`. They should report `resolved` after a successful retry using the same operation name. A hosted App must wait until its active `instanceId` is established before sending the report.

Homepage accepts the report only from the configured App origin and exact iframe source, slug, and instanceId. A failed report creates or updates one coordinated parent Toast and one live Warning per App instance and operation. A resolved report closes the Toast and removes the Warning. Navigation away from the App does not clear its parent Toast or unresolved Warning; reloading or replacing the App iframe clears that App-instance state. The active Warning is not stored in Homepage Preferences.

#### Toast bridge [v]

_Request feedback and handle one user action._

| App situation | Message |
| --- | --- |
| Ordinary user-facing event | `homepage/app/toast` |
| Named API failure or recovery | `homepage/app/api-status` |
| Toast action | `homepage/app/toast-action` from Homepage to the exact App instance |
| Hard App or data failure | App-owned fallback plus the appropriate report; Toasts are supporting UI. |

An App may request a parent-owned Toast after its active `instanceId` is established:

```json [v nowrap] Toast request
{
  "type": "homepage/app/toast",
  "slug": "example",
  "instanceId": "/home#example-1",
  "severity": "warning",
  "message": "Could not save changes.",
  "issueId": "save-failed",
  "issueState": "active",
  "action": { "id": "retry", "label": "Retry" }
}
```

| Field | Rule |
| --- | --- |
| `type`, `slug`, `instanceId` | Flat bridge identity; `instanceId` must be the active embedded instance. |
| `severity` | `info`, `success`, `warning`, or `error`. |
| `message`, `title` | Plain user-facing text; maximum 240 and 80 characters respectively. `message` may be omitted only when resolving an issue. |
| `duration`, `persistent` | Use one: bounded duration in milliseconds (1,000–10,000), or `persistent: true`. |
| `dedupeKey` | Optional stable identity for equivalent events; maximum 100 characters. |
| `issueId`, `issueState` | Required for warning/error Toasts. `issueState` is `active` or `resolved`; the ID is maximum 100 characters. |
| `action` | Optional single action. Its ID is maximum 64 characters using letters, numbers, `.`, `_`, or `-`; its label is maximum 48 characters. An optional `href` must be an internal Homepage path/hash or an absolute `http`/`https` URL. |

An active warning/error creates or updates the corresponding Warnings issue. A resolved report uses the same `issueId`, clears the issue, and does not create a new Toast. Info/success Toasts must not include issue fields. Homepage owns placement, timing, queueing, grouping, accessibility, and text truncation; Apps send no HTML, callbacks, unvalidated URLs, placement, or styling instructions.

When an action includes `href`, Homepage opens internal paths and hashes in the current tab and opens external `http`/`https` URLs in a new tab. Homepage handles that URL action locally and does not send `homepage/app/toast-action`. Without `href`, the action uses the App-owned `homepage/app/toast-action` flow described below.

When the user invokes an action, Homepage sends:

```json [v nowrap] Toast action
{
  "type": "homepage/app/toast-action",
  "slug": "example",
  "instanceId": "/home#example-1",
  "actionId": "retry",
  "issueId": "save-failed"
}
```

The App validates `event.source === window.parent`, the same-origin `event.origin`, slug, exact instance, expected issue, and known action ID. It ignores stale or unknown actions, performs the operation, and reports recovery separately. The command is not a callback or permission grant.

Toasts and unresolved issues survive Homepage navigation but are cleared when the App iframe is actually reloaded or replaced. Toast dismissal or expiry does not resolve an issue. Homepage may queue, group, rate-limit, or dismiss a request; an App must not depend on immediate Toast visibility. Standalone Apps must retain an App-owned fallback.

For a named API failure, the App sends only `homepage/app/api-status`; Homepage creates the coordinated parent Toast and Warnings issue. A matching resolved API-status report closes the Toast and clears the issue.

#### Preferences requests

App-owned Preferences remain App-owned. The Image Generator uses the existing sectionId: image-generation request for its output settings.

For a registered capability, an App may request the corresponding setup target with only the semantic capability ID:

```json [v nowrap] Capability Preferences request
{
  "type": "homepage/app/preferences-request",
  "slug": "image-generator",
  "instanceId": "/home#image-1",
  "embedMode": "inline",
  "action": "open",
  "capabilityId": "api:image-generator"
}
```

Homepage validates the iframe source, same-origin target, exact App slug, instance, and mode, then resolves the capability through the registered manifest metadata. The App cannot choose an arbitrary Preferences group, section, or credential ID. The Image Generator pilot resolves this request to the global APIs group and focuses openai-image.

A recovery report uses the same operation identity:

```json [v nowrap] Example recovery report
{
  "type": "homepage/app/api-status",
  "slug": "example",
  "instanceId": "/home#example-1",
  "operation": "forecast",
  "state": "resolved",
  "code": "provider_available",
  "message": "The forecast service is available again."
}
```

The baseline contract should omit `details` unless a reviewed App needs safe diagnostics. When used, details are limited to at most eight `{ label, value }` entries, with labels bounded to 60 characters and values bounded to 240 characters; labels and values must be allowlisted, user-safe, and free of credentials, authored secrets, request data, provider URLs, and raw upstream responses.

If the host does not yet support this extension, the App must retain a safe in-App fallback.

#### Compatibility and evolution [v]

- The manifest version is descriptive today. There is no automatic bridge version negotiation.
- Only names in the `homepage/app/...` namespace are part of the current protocol.
- Ignore unknown message types and fields. Do not treat an unknown field as permission or assume that an undeclared capability exists.
- Validate the fields an App uses, tolerate optional fields being absent, and keep repeated equivalent configuration idempotent.
- Keep App-specific messages namespaced and scoped to the exact App and instance. Do not reuse a message for a new meaning.
- A bridge change that affects other Apps belongs in the shared App/bridge requirement, its tests, and this document. An App-specific change also updates that App's requirement and distribution documentation.
- If a host does not support an extension, the App must retain a safe general-contract fallback.

### Preferences [v]

In Homepage-served contexts, Homepage Preferences are the normal place for persistent, user-facing App settings. The App defines the controls; Homepage renders, stores, discloses, and sends the values.

#### Default policy [v]

Use Homepage Preferences when a setting is:

- persistent across visits;
- a user-facing choice about the App's content, view, or App-specific navigation;
- user-facing rather than an internal implementation detail;
- useful while the user is viewing the App in Homepage;
- expressible as a simple supported control;
- better kept consistent with Homepage's global Preferences experience.

Keep a setting inside the App when it is:

- moment-to-moment selection, cursor, camera, or other interaction state;
- part of a task flow rather than configuration;
- a high-frequency interaction;
- a compound form that current host controls cannot express;
- content-specific and meaningful only inside the App canvas;
- direct-only behavior with no hosted equivalent;
- sensitive or permission-bearing behavior that needs a separate reviewed contract.

This is a design policy, not a technical restriction. An App may deviate when it documents why host Preferences would make the workflow less clear or less safe.

#### Hosted control duplication [v]

When an App contributes a hosted Preference, the embedded App must not show a second control that changes the same value. The App may show the resulting state or provide a different, non-duplicative action.

This applies to settings, view choices, and App-specific navigation exposed through Homepage Preferences. Direct mode may still show the App-owned control. A richer embedded control is an App-specific deviation and belongs in the App extension documentation.

#### When to deviate [v]

| Situation | Recommended treatment |
| --- | --- |
| Persistent display setting | Homepage Preference. |
| Temporary selected item, cursor, or route position | App-owned runtime state. |
| Large or multi-step configuration form | App-owned UI, possibly with a small persistent Preference for the default. |
| Primary task action | App-owned UI unless Homepage needs to coordinate it as a lifecycle command. |
| App-specific navigation that should integrate with Homepage Chrome | Hosted Preference or App-specific navigation extension, as Maps does with navigation-list. |
| Direct-mode-only convenience setting | App-owned direct storage; do not expect it in hosted mode. |
| Setting that requires a secret or authorization | Server-side API design, not a Preference control containing the secret. |

#### Preference structure [v]

A contribution contains collapsible sections:

| Section field | Meaning |
| --- | --- |
| id | Stable section identifier. |
| label | User-facing section label. |
| initialOpen | Initial disclosure state. |
| preferences | Controls in the section. |

Each Preference has:

| Preference field | Meaning |
| --- | --- |
| id | Stable identifier within the App instance. Use App-specific names. |
| label | User-facing label. |
| control | Current supported control type. |
| defaultValue | Value used when no saved value exists. |
| value | Optional current value in a contribution. |
| liveUpdate | Must be `true` when declared. Current controls apply immediately; `false` and deferred Apply/commit flows are not supported. |
| persistence | `homepage` lets Homepage store the value per `instanceId`; `app` leaves persistence to the App; `memory` survives neither remount nor reload. If omitted, the value is treated as `memory`. |
| min, max, step | Range constraints. |
| options | Select or radio options with value, label, optional symbol, and optional disabled. |
| reveals | IDs disclosed by a toggle's secondary disclosure control. |
| items | Maps-specific navigation items for navigation-list. |

#### Current controls [v]

The current Homepage renderer supports:

| Control | Current behavior |
| --- | --- |
| action | One-shot button; activation sends the value `activate` and is never stored by Homepage. |
| color | Color input. |
| navigation-list | Maps-specific ordered navigation buttons. |
| radio | Symbol-and-label button group; selection is sent immediately and is stored only when persistence is `homepage`. |
| range | Numeric range input. |
| select | Select input. |
| text | Text input; current host commits on blur. |
| toggle | Boolean checkbox with optional reveals disclosure. |

The shared target vocabulary also mentions checkbox, date, datetime, email, number, password, textarea, time, and url. Those are not current Homepage-rendered App controls. Use the seven general controls above. `navigation-list` is Maps-specific and requires the Maps extension.

#### Lifecycle and storage [v]

- Manifest contribution is normalized when an embedded instance is mounted.
- A runtime contribution replaces the current contribution for the same App and instance.
- Empty sections are removed. A navigation-list without usable items is removed.
- Homepage stores only non-action Preferences whose persistence is `homepage`, keyed by `apps.instances[instanceId]` in its signed HTTP-only settings cookie.
- `app` is owned by the App. Homepage does not save it or reuse an old host-side value.
- `memory` is not persisted by either side unless the App separately chooses to persist it.
- The initial value is resolved in this order: saved `homepage` value, contribution `value`, `defaultValue`, then an empty value.
- Only Homepage-owned saved values are sent in `homepage/app/config.preferences`; individual changes use `homepage/app/preference`.
- Current controls apply immediately. Range, color, toggle, radio, and select changes send on change; text sends on blur. There is no generic Apply or commit message.
- An action sends a one-shot `activate` value. The App owns its result and any App-owned reset. Homepage has no generic reset command for stored App Preferences; resetting a Homepage-owned value needs a separately reviewed extension.
- Apps do not read the Homepage settings cookie directly.
- `/app/<slug>` is a Homepage-served direct App surface and can use Homepage Preferences. Opening the App distribution outside Homepage has no Homepage-owned storage.

### APIs and data [v]

#### Security boundary [v]

Homepage Apps are first-party, curated distributions created by App Coder and reviewed before integration. They are not third-party plugins.

Hosted Apps currently run in a same-origin, unsandboxed iframe. The bridge validates messages, but it is not a sandbox or permission boundary. Treat App client code as trusted Homepage code: review it before integration, keep secrets out of `dist/`, and use named Homepage APIs for server-backed work.

#### Access matrix [v]

| Data or capability | App access today |
| --- | --- |
| Authored Embed configuration | Yes, through homepage/app/config, field config. |
| Saved hosted Preference values | Yes, through homepage/app/config.preferences. |
| Live Preference changes | Yes, through homepage/app/preference. |
| Theme, brightness, accent, CSS variables | Yes, through host context. |
| Embedded safe area | Yes, through layout context. |
| Homepage DOM or undeclared same-origin browser storage | Technically accessible under the current trusted-App model; not a supported contract. |
| Declared App-owned extension stores | Only through the exact reviewed extension contract; the App remains canonical owner. |
| Homepage settings cookie | No direct read; it is HTTP-only. |
| OpenAI API key stored by Homepage | No. |
| Provider API keys or secrets | No. |
| Login/session data | No current App contract. |
| Curated Homepage API | Only through the named route's documented request/response contract. |

Apps must not depend on direct DOM or storage access. Same-origin access is an implementation consequence of the current trusted-App model, not a permission grant. Use explicit bridge and API contracts instead.

#### Curated APIs [v]

Homepage provides a named API when repeated use, provider abstraction, normalization, server-side credentials, or security makes a capability worthwhile.

An App must not:

- accept an arbitrary provider URL;
- forward arbitrary request bodies to an external service;
- infer an API route from a manifest declaration;
- put a provider key in browser code;
- assume that an API exists because another App uses it.

#### Calling a curated API [v]

Maps is the current example of a named same-origin Homepage API. This call shows the general request shape; the Maps route catalogue appears in the Maps reference:

`js [< nowrap] Calling a Homepage API
const response = await fetch("/api/maps/address", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ address: "Berlin, Germany" })
});

const result = await response.json();

if (!response.ok || !result.ok) {
  throw new Error(result.error?.message || "Homepage API request failed.");
}

const point = result.data;
`

The current response envelope is:

```json [v nowrap] API result envelope
{
  "ok": true,
  "data": {},
  "error": null
}
```

Failures use ok: false, data: null, and an error object containing a stable code, a user-safe message, and optional details. When an App surfaces that failure to the host, it should report only a bounded safe summary through `homepage/app/api-status`; provider credentials, raw upstream payloads, authored secrets, and arbitrary URLs must stay out of the bridge.

#### Image Generator API [v]

The Image Generator uses the named same-origin route `POST /api/image-generator`. The route is a single-purpose Homepage adapter; it is not a generic provider proxy. Its request uses only provider-neutral values:

```json [v nowrap] Image Generator request
{
  "operation": "generate",
  "prompt": "A blue glass sculpture",
  "referenceImages": [{ "mimeType": "image/png", "base64": "..." }],
  "options": {
    "modelPolicy": "default",
    "outputFormat": "png",
    "size": "square",
    "quality": "high",
    "background": "auto"
  }
}
```

`operation` is `generate` or `edit`; edit requests may include `sourceImage`. The allowed semantic values are `default|fast|quality`, `png|jpeg|webp`, `auto|square|landscape|portrait`, `auto|low|medium|high`, and `auto|opaque|transparent`. Homepage resolves the logical credential `openai-image` server-side, maps the model policy to an allowlisted provider model, and never sends the credential into the App iframe.

Successful responses contain exactly one normalized image at `data.image`, with `mimeType`, `base64`, and an optional `revisedPrompt`. Provider model IDs, arbitrary URLs, raw upstream bodies, and provider credentials are not accepted or returned.

### App extensions [v]

The general contract is the default. An App-specific requirement is appropriate when the App has a real need that the general capabilities cannot express clearly or safely.

#### When to extend [v]

Use an extension when:

- the behavior is meaningful to the App's domain rather than most Apps;
- the host needs to coordinate it with Homepage Chrome, Slides, or other host-owned navigation;
- a repeated server operation deserves a named curated API;
- a general capability would become ambiguous or overloaded if it absorbed the behavior;
- the App needs a richer interaction than current Preferences controls can express.

Do not add an extension merely to avoid using an existing general capability.

#### Extension contract [v]

An App-specific extension should document:

- the user need and why the general contract is insufficient;
- whether the extension is direct-only, inline-only, full-only, or multi-context;
- manifest declaration and support detection;
- message or API names;
- direction, trigger, fields, validation, and failure behavior;
- exact slug and instanceId rules;
- ownership between App and Homepage;
- a compact example;
- how the extension behaves when the host does not support it.

The App-specific requirement belongs beside the App's source and distribution. Homepage integration requirements should also be reflected in the shared App/bridge requirement before the host changes.

#### App-owned history extension [v]

An App may declare an explicit app-owned Local Storage extension when the App remains the canonical owner of a domain history while Homepage must render a lightweight host surface even when the App iframe is unloaded. The Image Generator is the current example.

The manifest declares `distribution.extensions.appOwnedHistory` with schema version `1`, the history key `app.image-generator.history.v1`, the reference key `app.image-generator.references.v1`, and the metadata key `app.image-generator.metadata.v1`. Homepage reads only this declaration, validates the exact App slug, exact `instanceId`, and schema version, and does not migrate or reinterpret the App's stores.

The App emits `homepage/app/image-generator/history-changed` with its exact `instanceId`, `schemaVersion`, and numeric `revision`. Homepage sends `homepage/app/image-generator/history-command` to the active matching instance with `command: refresh|select|open` and an `entryId` for selection/opening. Host-side actions such as preview, download, prompt copy, favorite, delete, and clear may operate on the declared stores while the App is unloaded; after a host mutation, Homepage sends `refresh` to a loaded matching instance.

The App owns history entries, references, builder snapshots, and metadata. Homepage owns only the host presentation and validated coordination. When `metadata.hasGenerated` is true, the Images Preferences box remains available after the App is unloaded; it is closed by default when the App is hidden and open by default while the matching App is embedded. Unsupported hosts ignore the extension and the App continues to work in its own surface.

#### Extension template [v]

Use this compact structure for a new App-specific requirement:

```text [v] App extension template
Extension: <name>
Need: <user or host problem>
Scope: direct | inline | full | multiple
Declaration: <manifest or Embed declaration>
Messages/API: <names and direction>
Fields: <required fields and valid values>
Lifecycle: <trigger, readiness, updates, teardown>
Validation: <accepted, ignored, and failed inputs>
Fallback: <behavior when the host or extension is unavailable>
Ownership: <what App Coder owns; what Homepage Builder owns>
Tests: <contexts, reloads, failures, and security cases>
```

#### Maps extension [v]

Maps extends the general contract with navigation-list hosted Preference items, route-navigation, route-painted, route-failed, route-navigation-state, route API warning adaptation, and route-readiness/lifecycle coordination.

These are intentionally Maps-specific host behaviors, not baseline requirements for another App. Homepage does not provide a generic extension framework for them yet; that work is deferred until another App has a comparable host-coordination need.

## Verify and publish [v]

_Distribute the App as a standalone file or as part of the Homepage._

### Testing and debugging [v]

#### Local integration loop [v]

When debugging a local integration, follow the [main integration workflow](#build-and-integrate-an-app) and use the [local test contexts](#local-test-contexts) for the URLs. Then run this diagnostic loop:

1. Confirm that current App source is built or synchronized into `work/apps/<slug>/dist/` and visible through the local `work/apps` symlink.
2. Run `make generate`; if it fails, inspect `work/backend/reports/latest.md` before trusting generated artifacts.
3. Verify that the App slug appears in the generated registry before debugging an iframe or bridge.
4. Run the local Codex preview on port 5174 and verify `/app/<slug>` directly before adding an authored Embed.
5. Test one inline Embed and, when supported, one full Embed. Inspect bootstrap context, live bridge messages, Preferences, reloads, transitions, and failure states.
6. Run the App's own tests in the App workspace and Homepage Builder's host tests after changing the integration contract.

The current workspace commands are:

`sh [v nowrap] Homepage Builder local loop
make codex-dev
make generate
make test
`

These are Homepage Builder commands. App Coder runs the App's own test suite with the App workspace's tooling.

#### Test matrix [v]

| Test | Verify |
| --- | --- |
| Distribution and registry | Refreshed `dist/` is visible through `work/apps`, generation succeeds, and the slug is present in the generated registry. |
| Direct route | The Homepage route `/app/<slug>` renders as a managed full-surface App with its expected bridge messages, route-scoped instanceId, and Preferences; it does not rely on standalone-only behavior. |
| Standalone distribution | The App's own standalone test environment renders when the App supports one; this is not a Homepage mode. |
| Inline Embed | Configuration, Preferences, height, safe area, and resize behavior. |
| Full Embed | Full layout, Homepage Chrome, lifecycle behavior, and screen presentation if supported. |
| Initialization handshake | `host-ready` and `ready` use the exact slug, embedMode, and instanceId; replayed snapshots are idempotent; timeout, reload, and teardown behavior is safe. |
| Bridge conformance | Homepage host tests reject malformed, wrong-origin/source/path, wrong-slug/instance, and stale messages; reloads reprocess the current configuration; failure, recovery, and Toast reports remain instance-scoped. |
| Reload | Initial bootstrap, listener installation, repeated configuration, and restored Preferences. |
| Content parser | Unquoted `//` is removed as a comment; quoted URLs and other values containing `//` reach the generated configuration intact. |
| Live Preference | The relevant App state changes without iframe reload, new instance, repeated initial configuration, or initial-loading presentation. |
| Hosted presentation transition | The App follows Homepage's currently displayed accent and text colors throughout a transition without an early target jump or a second local transition. |
| Embedded surface | The App and any owned canvas or renderer follow Homepage's background instead of showing a standalone fallback. |
| Missing or malformed input | Safe App behavior without uncaught runtime errors. |
| Toast request validation | Valid info/success and active warning/error requests are accepted; invalid severity, missing message, and malformed issue fields are rejected, text and numeric limits are normalized or clamped, and wrong source/origin/slug or instance are ignored. |
| Toast issue lifecycle | An active warning/error creates or updates one issue; a matching `resolved` report clears it without creating a new Toast; dismissal and expiry do not resolve it. |
| Toast action | Homepage sends only the exact `actionId` and instance; the App validates source, origin, slug, instance, issue, and known action before acting. |
| Toast navigation and teardown | Navigation preserves App Toasts and unresolved issues; an actual iframe reload or replacement clears the App-instance state. |
| API warning lifecycle | The App reports safe failure and recovery; Homepage creates or removes one transient Warning per instance and operation. See [Outbound: API status](#outbound-api-status). |
| API warning validation | Reports from the wrong source, origin, slug, or instance are ignored; equivalent reports remain one Warning. |
| Missing API or provider failure | The App shows its fallback and reports a safe Warning when appropriate; credentials and raw upstream detail stay private. |
| Theme and brightness change | App updates from live host context. |
| Motion and host-only fields | Current Apps use local reduced-motion fallbacks. `reducedMotion`, `textScale`, and `devMode` are not current bridge capabilities; do not infer them from the URL. |

#### Debugging [v]

- Inspect the App iframe URL and confirm its embed context.
- Inspect initial hostContext only as bootstrap input; verify live host-context messages separately.
- Verify the generated registry and distribution before debugging iframe or bridge behavior.
- For hosted transitions and backgrounds, apply the [Host presentation timing](#host-presentation-timing) rules.
- Log message type, slug, and instanceId without logging credentials or authored sensitive data.
- Treat unknown messages and rejected payloads as ignored host traffic, not as an implicit permission.
- Test the direct App route before debugging an embedded failure.
- Follow [Failure triage](#failure-triage) and investigate the earliest broken boundary first.
- Keep App-specific diagnostic detail inside the App or its reviewed extension contract.

### Publishing [v]

#### From dist to public route [v]

1. App Coder produces the distribution.
2. Homepage Builder discovers the matching dist/manifest.json.
3. The generator creates the App registry and asset inventory.
4. Deployment materializes the App distribution in the published runtime.
5. The same distribution serves direct and embedded routes.

The published asset route currently uses Cache-Control: no-store. The App entry and assets are served from the authored distribution; App source, tests, documents, and logs are not public runtime assets.

#### Public routes [v]

For an App with slug maps:

| Use | Route |
| --- | --- |
| Canonical Homepage App route | /app/maps |
| Distribution entry | /app/maps/index.html |
| Deployed canonical route | https://www.<domain>/app/maps |
| App-focused deployment alias | https://apps.<domain>/maps |

The alias resolves through the same Frontend runtime; it is not a second App copy. Current App routes are public when the deployment is public. Gated App behavior belongs to the separate access-control contract.

#### Publication checklist [v]

- dist/manifest.json is valid and the slug matches its directory.
- The declared entry exists inside dist.
- Required runtime assets are present.
- Direct mode works before embedded modes are tested.
- Supported contexts are declared accurately.
- No browser-shipped secret is required.
- API routes are explicit and curated.
- Bridge messages use the correct App slug and exact instance identity where required.
- Public route and App listing metadata are correct.

## Appendix [v]

_Supportive material._

We use the "Maps" app as [reference implementation](#maps-reference).
There are still [open questions](#open-design-questions).
We close with our [integration checklist](#integration-checklist).

### Maps reference [v]

Maps is the current complete example of the general contract plus deliberate extensions:

- It runs direct, inline, and full; its distribution includes a manifest, entry document, runtime assets, and a pre-paint bootstrap.
- It waits for `homepage/app/config` before preparing hosted content, uses Homepage Preferences for persistent settings, and keeps route position and task navigation in App state.
- It consumes named same-origin address, point, route, and area APIs.
- It reports named API status and route events, registers lifecycle controls for the active instance, and exposes Maps-only route navigation. Homepage adapts API status and route-failed/route-painted reports into the shared Preferences Warning lifecycle while Maps keeps its own detailed fallback message. See [Outbound: API status](#outbound-api-status) for the general warning contract.

#### Maps API routes [v]

| Method | Route | Request body | Current purpose |
| --- | --- | --- | --- |
| POST | /api/maps/address | { address: string } | Resolve an address to a point. |
| POST | /api/maps/point | { point: [latitude, longitude] } | Validate and normalize coordinates. |
| POST | /api/maps/route | { from: location, to: location, mode: string } | Resolve one route leg. |
| POST | /api/maps/area | { area: string } | Resolve a named area boundary. |

These Maps routes keep provider details and credentials on the server. The future API offering will be authoritative for route schemas, errors, caching, rate limits, and provider behavior.

#### Maps failure matrix [v]

The Maps extension keeps provider-specific behavior inside the Maps/API contract. The general Apps contract only needs the ownership and user-visible outcome:

| Failure | Maps behavior | Homepage behavior |
| --- | --- | --- |
| Address or point resolution fails | Shows a safe location error; no location is prepared and no route fit is attempted. | Adds the corresponding transient Preferences Warning and keeps the host state from claiming that usable map content is ready. |
| Named area resolution fails | Shows an area error; the affected area framing is not prepared and an affected route may report route failure. | Adds the corresponding transient Preferences Warning, handles the host loading state, and accepts the App's safe outcome. |
| Routing provider fails but direct fallback is possible | Paints a dashed direct-line fallback and shows a safe warning. This is a successful route paint, not `route-failed`. | Adds or updates the route API Warning and stops waiting for route readiness once Maps reports the fallback paint. |
| Maps emits `homepage/app/maps/route-failed` | Keeps the detailed warning in Maps, reports the safe failure with the active instance, and preserves the route-specific report. | Adds or updates the parent-owned route Warning and clears its route-loading overlay; it does not replace the App's domain-specific warning. |
| Homepage route-loading timeout | Maps may continue preparing or resolving content. | After four seconds, stops blocking the embedded surface; timeout is not a success signal. |

The Maps API implementation owns provider selection, required request headers, upstream timeouts, fallback policy, and provider diagnostics. Those details belong in the Maps/API documentation and the future API offering, not in the general Apps contract.

#### Maps example [v]

```json [< nowrap] Maps Embed configuration
{
  "app": "maps",
  "mode": "inline",
  "address": "Berlin, Germany",
  "route": {
    "from": "Hamburg",
    "travel": [
      { "mode": "train", "to": "Berlin" }
    ]
  }
}
```

The App receives authored values as config, saved values as preferences, and the generated instanceId through homepage/app/config. It contributes Route navigation and Map settings through homepage/app/preferences, then uses the Maps extension messages when the route is ready.

Maps-specific behavior remains in the Maps distribution README and requirements. This section explains why the App is a useful reference; it is not a replacement for Maps documentation.

### Open design questions [v]

- **Bridge versioning:** Deliberately deferred. Homepage and the current Apps ship as one coordinated first-party bundle, so one unversioned bridge is sufficient for now. Revisit this before Apps can be released independently or multiple bridge versions must coexist; define versioning and capability discovery together at that point.
- **Bootstrap sharing:** Maps, Models, and CX Widgets currently carry App-local pre-paint bootstrap scripts. A shared runtime package is deferred until the separate App distribution workflow can support it without adding unnecessary packaging complexity.
- **Host context coverage:** `reducedMotion`, `textScale`, and `devMode` are not part of the current generic host-context contract. Current App distributions must not declare or depend on them. Revisit them only when a concrete shared need justifies a coordinated host-and-App change.
- **Manifest capabilities:** context support, bridge participation, and required assets are enforced; parent-context fields, bridge-message declarations, and API/resource declarations remain descriptive until their contracts are mature enough to enforce.
- **Preference semantics:** current `homepage`, `app`, `memory`, immediate-update, and action rules are enforced. Deferred commits and a generic reset for Homepage-owned values are deliberately not part of the contract.
- **Maps-specific host code:** Maps route navigation, route loading, and navigation-list remain intentional host special cases. Generalization is deferred until another App needs comparable coordination.
- **API offering:** The stable catalogue of Homepage APIs, schemas, errors, caching, and provider policy is deferred to a separate document.
- **Older design notes:** Commands such as nextSlide, previousSlide, goToRoute, goToPresentation, setBrightness, setBrandTheme, and openExternal are not part of the current bridge.

#### Next design pass: tooling [v]

These are follow-ups, not current App requirements:

- **Distribution freshness:** Define the authoritative source-to-`dist/` build or synchronization step, then check that `dist/` reflects current source and contains its required runtime assets. Do not require byte-for-byte equality when the build transforms files.
- **Registry smoke test:** After generation, verify that every discovered App slug appears in the generated registry at `work/frontend/content/apps.ts` and has a matching route and asset inventory.
- **Accent parity:** Add a browser regression that compares Homepage and App displayed values during initial load, reload, inline mode, and full mode, while keeping standalone mode independent.
- **Stale-artifact diagnostics:** Make generation failure and stale generated-artifact state prominent enough that existing generated files cannot be mistaken for a successful current build.

### Integration checklist [v]

- [ ] dist/manifest.json is valid; its slug, entry, supported contexts, bridge declaration, and required assets are correct.
- [ ] `parentContext` declares only fields currently delivered by the general host context; unsupported fields are not treated as available capabilities.
- [ ] The App works at its direct route before hosted behavior is tested.
- [ ] Hosted startup installs a listener early, treats instanceId as opaque, and reads authored values from config.
- [ ] Hosted startup completes the `homepage/app/host-ready` / `homepage/app/ready` handshake for the exact instance; replay, timeout, reload, and teardown are safe.
- [ ] Bootstrap styling is optional; later updates come from live host-context messages.
- [ ] Hosted Preferences use current controls, receive initial values, and handle live changes.
- [ ] Each Preference uses `liveUpdate: true` or omits it, and chooses `homepage`, `app`, or `memory` deliberately; action controls are one-shot and never persistent.
- [ ] Hosted presentation styling uses Homepage's displayed values and background without a competing local transition.
- [ ] Provider access uses a named Homepage API route; no secret ships in dist.
- [ ] Relevant hosted App events use `homepage/app/toast`; Toast text is plain, bounded, user-facing, and does not contain HTML, secrets, raw provider responses, unvalidated URLs, or styling instructions.
- [ ] Warning/error Toasts use stable `issueId` and `issueState`; recovery reports resolve the same issue identity.
- [ ] Toast actions use one stable action ID; App-owned actions validate `homepage/app/toast-action` against the exact parent, origin, slug, instance, issue, and known action, while URL actions use only the bounded internal/HTTP(S) target contract.
- [ ] API failures and recovery use `homepage/app/api-status` with safe bounded diagnostics; no secret, raw provider response, authored sensitive value, or arbitrary URL crosses the bridge.
- [ ] The internal App security review checklist is complete for the submitted dist/ output.
- [ ] Direct, inline, full, reload, malformed-input, Toast, action, and failure cases are tested.
- [ ] App-specific extensions document scope, validation, fallback, and ownership.
- [ ] Maps-specific messages are used only by the Maps integration.
- [ ] The change identifies whether it belongs to App source/`dist/`, Homepage bridge/generator code, or a Homepage API route.
- [ ] App and Homepage changes are reviewed separately; any Homepage source change has explicit Owner clearance before implementation.

This checklist is intentionally short. The sections above are the contract reference when an App needs field-level detail.
"""
