Skip to content

Hosted app contract (URL bootstrap and integration manifests)

Audience: Agent, integrator, maintainer

This document is the portable EDOS contract for starting a hosted web app with predictable options (embedding, layout, feature toggles) and for publishing what each app supports. It applies to first-party EDOS apps and to external or self-hosted apps that choose to implement the same contract—without subscribing to a discovery registry or a monorepo checkout.

Relationship to IPC.md

Topic Where it is specified
postMessage shapes for journal lines, status, and edos-file-event via @howfe/inter-frame-messenger (MessageTypeEnum, allowlists, envelopes) IPC.md and apps/inter-frame-messenger.md
URL query parameters, embedding profiles, machine-readable integration manifests, and optional embed handshakes that are not MessageTypeEnum values This document
Sibling iframe IPC under a composition host (request/response proxied through the parent; not MessageTypeEnum) This document § Composition host and sibling iframes

Integrators need both when a shell loads a URL and then talks journal or file data over IFM: bootstrap the UI from the URL (here); stream game data over IFM (IPC.md). Cross-app reads of another app’s origin-local state (for example commander bookmarks) use the composition proxy pattern here—not IFM and not a hidden iframe to the provider origin.

Scope

In scope: How a host passes initial options (query string or equivalent), where to fetch a manifest on the app’s own origin, optional runtime handshake over postMessage, naming rules for shared vs app-specific keys, in-app navigation views (sub-navigation) for host discovery and external control, composition-host proxy message types for sibling iframes, contract versioning, and precedence rules.

Out of scope: Federated app discovery registries (optional indexes—specified separately), composition-host product behavior (menu layout, journal fan-out rules, distribution UI—see apps/elite-dangerous-app-composition.md), authentication protocols, full CSP matrices, and implementation details of @howfe/inter-frame-messenger beyond cross-links.

Definitions

Term Meaning
Host Any environment that loads the app (browser tab, iframe, Tauri WebView, another community tool).
Bootstrap URL The document URL the host loads (https origin + path + query + fragment as applicable).
Manifest JSON document describing supported bootstrap keys, entrypoints, and optional postMessage types for discovery and tooling.
Navigation view A shell-level panel selected by in-app sub-navigation (tabs, segmented control, top-bar links). Distinct from table views in TABLE_GUIDANCE.md (saved column/filter presets inside a data grid).
Handshake Optional postMessage from child to host after the app is ready, advertising build identity and capabilities.
Composition host A host that loads multiple EDOS apps in sibling iframe slots and mediates postMessage between them (today: EDAC, elite-dangerous-app-composition).
Provider app The iframe that owns origin-local data and answers a query (for example Universe Places / edup for bookmarks).
Consumer app The iframe that initiates a proxied query via the composition host (for example Stellar Scan / edss).

Composition host and sibling iframes

Problem

Each hosted app stores data on its own origin (localStorage, in-memory state). Sibling iframes under a composition host cannot read another app’s storage and should not postMessage peer iframes directly—there is no shared Window reference and no stable security model for arbitrary peer routing.

Anti-pattern (forbidden for EDOS first-party apps): a consumer embedding a hidden iframe to another app’s origin to reach that app’s local data. Use the composition host proxy instead.

Normative pattern

sequenceDiagram participant Consumer as Consumer slot iframe participant Host as Composition host (top window) participant Provider as Provider slot iframe Consumer->>Host: edac.*.query (proxy request) Host->>Provider: provider-native query (e.g. edos.places.query) Provider->>Host: provider-native result Host->>Consumer: result or edac.*.queryResult error

Rules:

  1. The consumer posts only to window.parent (the composition host), using a host-specific proxy type (today edac.places.query).
  2. The host validates event.origin against slot URLs in the active composition, resolves a provider slot, and forwards the provider’s documented query type to that iframe’s contentWindow.
  3. The provider answers on the host window; the host correlates requestId and forwards the provider result to the consumer iframe (postMessage with the consumer’s origin as targetOrigin).
  4. The provider slot must be mounted (page slot, overlay, or backgroundSlots[] in EDAC)—visibility is not required.
  5. Standalone consumer tabs (not under a composition host) must not simulate this with hidden provider iframes; operators use a composition layout that includes both apps, or open the provider app directly (URL bootstrap / deep links only).

Journal fan-out from the composition host is a separate broadcast pattern (IPC.md, EDAC journal hub)—not a substitute for request/response queries.

Universe Places bookmark query (first proxy)

Provider: elite-dangerous-universe-places (edup.*) — implements edos.places.query / edos.places.queryResult on its origin (Universe Places README).

Consumer → composition host:

{
  "type": "edac.places.query",
  "requestId": "unique-per-in-flight",
  "query": { "systemAddress": 10477373803 },
  "limit": 50,
  "targetSlotId": "optional-slot-id",
  "targetRegistryAppId": "elite-dangerous-universe-places"
}
Field Meaning
query Provider PlaceQuery (systemName, systemAddress, bodyName, kind, text, …)
targetSlotId Optional explicit provider slot
targetRegistryAppId Optional; default elite-dangerous-universe-places

Host → provider: same requestId and query as edos.places.query (routing fields omitted).

Success (host → consumer): passthrough edos.places.queryResult (requestId, count, entries).

Failure (host → consumer):

{
  "type": "edac.places.queryResult",
  "requestId": "same-id",
  "ok": false,
  "error": "no-target-slot"
}

error: no-target-slot, target-not-ready.

Provider allowlist for direct queries: composition host origin (e.g. https://edac.howfe.org) — not consumer origins. EDAC implementation detail: elite-dangerous-app-composition/docs/COMPOSITION_CONTRACT.md.

Future proxied capabilities should follow the same shape: edac.<domain>.<verb> from consumer to host, provider-native types on the provider iframe, host-mediated correlation by requestId.

Contract version

Hosts and apps SHOULD agree on a contract revision.

  • Query parameter (optional on any URL): edos.contract=<revision>
    Suggested values: monotonic integers (1, 2, …) or dated slugs (2026-01). Pick one style for the ecosystem and keep it stable in prose and manifests.
  • Manifest field: contract MUST match the revision the manifest body was written for.

Rule: Adding optional manifest fields or new optional query keys is non-breaking for a given contract revision. Removing keys, changing types, or changing meaning is breaking and MUST bump contract.

Until manifests are ubiquitous, hosts MAY omit edos.contract and rely on app defaults documented in each app’s capsule.

URL bootstrap

Transport vs semantics

  • Transport: Where parameters live (path, location.search, hash fragment after #, etc.) depends on the app’s router (for example Vue hash mode keeps some queries in the pre-hash search string). Each app capsule MUST document where it reads bootstrap parameters.
  • Semantics: The meaning of each key is defined by this contract (shared) or by the app (prefixed / documented in the manifest).

Shared keys (namespacing)

  • Target convention: Shared keys that mean the same thing across multiple EDOS apps SHOULD use a common prefix, for example edos. (e.g. edos.mode=embedded). Exact spelling is fixed when first introduced in a contract revision.
  • Transitional key: Today several apps implement edosEmbedded (no dot) for “embedded / chrome-reduced” behavior. New shared keys SHOULD use the edos. prefix; apps MAY accept both old and new names during a migration window. Per-app capsules list truth in code.

Profiles

Prefer named profiles over many interacting booleans, for example edos.mode=embedded. A profile bundles UI rules (hide install button, hide “open remote reader,” etc.). Apps MAY expose profile-specific overrides in the manifest.

Optional compact configuration (future)

For long option sets, apps MAY support a single encoded blob (for example edos.cfg=) with documented encoding, size limits, and precedence vs flat query keys. Until specified per app, treat this as reserved.

In-app navigation views

Apps that expose sub-navigation between shell-level panels (for example Helm / Archive tabs) SHOULD publish those panels for host discovery and SHOULD map each panel to a URL path (or hash route) so deep links, reloads, and web servers behave like ordinary websites.

UX context: When to use sub-navigation vs collapsibles is guidance in DESIGN_GUIDANCE.md — Layout patterns. This section is the machine-readable contract for hosts and tooling.

Path routes vs query parameters

Use Preferred transport Why
Which shell panel (Helm, Archive, …) Path (or hash path after #) Matches how SPAs and static hosts route; shareable bookmarks; server try_files / nginx fallbacks work as users expect.
Cross-cutting host options (edos.mode, journal bootstrap, table preset ids, …) Query (or pre-hash search where the router requires it) Same entry document; options compose without inventing fake path segments for every toggle.

Normative: The canonical deep link for a navigation view is https://<origin><path>?<host-options>, not ?edos.view=<id> alone. Example: https://edep.howfe.org/archive?edos.mode=embedded.

Apps SHOULD implement client-side routing (history API or documented hash mode) so each manifest view has a stable path. When the user switches tabs, the app SHOULD update pathname (or hash path), not only in-memory state.

Distinction from table views

Concept Meaning Contract
Navigation view Top-level app shell panel (tabs, segmented control) This section — manifest navigation with per-view path; optional query alias
Table view Saved grid preset (columns, sort, filters, highlights) TABLE_GUIDANCE.md — app-specific query keys (for example edos.tableView) documented per app

Do not overload navigation paths or edos.view for table presets. Hosts must be able to open /archive?edos.tableView=… (example) when both features exist.

Path routing (normative)

Property Rule
path per view Each manifest view SHOULD declare a path (leading slash, app-relative, for example /, /archive). Exactly one view SHOULD use / or be marked default: true.
Unknown path SHOULD fall back to the default view (in-app redirect or replaceState to default path).
Hash routers Document paths after # (for example #/archive). Capsule MUST state history vs hash mode.
Static hosting Production deploy SHOULD serve index.html for unknown paths under the app (SPA fallback) so /archive works on refresh.
Host iframe URL Hosts SHOULD set iframe.src (or top-level location) to the view path plus any needed query options — not rely on query-only navigation when paths are published.

Optional query alias: edos.view

For hosts that cannot rewrite paths, or during migration, apps MAY accept edos.view=<id> where id matches navigation.views[].id.

Property Rule
Precedence If both path and edos.view are present and disagree, path wins unless the app documents otherwise in the manifest.
Canonical URL On load with only edos.view, the app SHOULD replace the URL with the matching view path (preserve other query keys).
Stability id remains the stable programmatic identifier (postMessage, handshake); path is the user-facing route.

When an app implements sub-navigation, the integration manifest SHOULD include a top-level navigation object:

Field Type Meaning
routerMode string Optional. history or hash — how path values are applied.
queryAlias string Optional. Query key for view id when path cannot be set; SHOULD be edos.view if implemented.
views array At least one object describing each selectable shell panel.

Each entry in views:

Field Type Meaning
id string Stable identifier for runtime messages (for example helm, archive).
path string Canonical app-relative route (for example /, /archive).
label string Human-readable title for host menus and tooling.
description string Optional short explanation of what the panel contains.
default boolean Optional. Exactly one view SHOULD be default when multiple views exist.

Example fragment:

{
  "navigation": {
    "routerMode": "history",
    "queryAlias": "edos.view",
    "views": [
      { "id": "helm", "path": "/", "label": "Helm", "default": true },
      { "id": "archive", "path": "/archive", "label": "Archive", "description": "Saved routes" }
    ]
  }
}

Entrypoint URLs in the manifest MAY point at specific paths (for example https://edep.howfe.org/archive) when a host always opens one panel.

Runtime control (optional)

Initial load is driven by the URL path (and query options). Hosts that need to change the navigation view after load MAY use non-IFM postMessage types listed in the manifest postMessage array:

Direction Suggested type Payload (minimal)
host → app edos.navigation.set { "type": "edos.navigation.set", "viewId": string }
app → host edos.navigation.changed { "type": "edos.navigation.changed", "viewId": string, "path"?: string }

Rules if implemented:

  1. viewId MUST match a manifest navigation.views[].id or be ignored.
  2. The app SHOULD navigate to that view’s path (and update history) when the view changes — user action, host message, or query alias resolution.
  3. The app MAY emit edos.navigation.changed after user-driven switches so hosts can sync surrounding chrome.
  4. Types MUST NOT collide with MessageTypeEnum strings unless coordinated via IPC.md and @howfe/inter-frame-messenger.
  5. Sibling iframe queries MUST use the composition host proxy; list proxy request types in the consumer manifest when the consumer initiates them, and provider query types in the provider manifest.

Handshake payloads MAY include navigationViewId and navigationPath (current panel after bootstrap) alongside existing fields.

Integration manifest (conformance)

An app that claims conformance to this contract MUST serve a JSON integration manifest at a stable path on the same origin as the hosted UI (the origin of the iframe or top-level document).

Discovery URL (normative):

https://<app-origin>/.well-known/edos-integration.json

The response SHOULD use Content-Type: application/json. Unknown top-level fields in the file MUST be ignored by consumers so the schema can evolve.

Manifest — required fields

Field Type Meaning
appId string Stable identifier (often matches the workspace folder slug, e.g. elite-dangerous-surface-map).
name string Human-readable title.
contract string or number Revision of this hosted-app contract the file was written for (parallel to edos.contract in URLs).
entrypoints array At least one object: { "id": string, "url": string, "description"?: string } with HTTPS url values for loadable UIs.
Field Type Meaning
documentationUrl string Human-readable integration page (often the EDOS capsule or project README).
bootstrap object Supported query keys: names, types, allowed values, defaults, stability (stable / experimental).
navigation object When the app has shell sub-navigation: per-view path routes and optional edos.view query alias — see In-app navigation views.
postMessage array Optional catalog of non-IFM embed messages (type, direction host-to-app / app-to-host, short description). IFM journal types stay in IPC.md.
handshake object If implemented: message type, timing (“after first paint / ready”), and payload field list.

Manifest — optional handshake declaration

If the app implements a runtime handshake, the manifest SHOULD include a handshake object so hosts can subscribe before load. The handshake itself remains optional (see below).

Optional runtime handshake

Apps MAY send a one-shot postMessage after they are ready, so the host learns build identity and narrowed capabilities without fetching the manifest first.

Requirements if implemented:

  1. Use a dedicated type string (for example edos.handshake) in a small JSON payload; this is not part of MessageTypeEnum unless maintainers deliberately add it to @howfe/inter-frame-messenger (then IPC.md and all consumers MUST be updated in lockstep).
  2. Payload SHOULD include at least: appId, contract, buildId (or semver).
  3. Payload MAY include: capabilities (array), manifestPath (e.g. /.well-known/edos-integration.json), navigationViewId (active shell panel after bootstrap), or a content hash for drift detection.

Hosts MUST validate event.origin before trusting handshake content.

Precedence (normative when multiple sources exist)

When a profile, flat query keys, and an optional edos.cfg blob could all apply:

  1. Explicit query parameters override profile defaults unless a key’s definition says otherwise.
  2. Optional blob (when specified by the app) overrides defaults for keys it contains; conflict rules with explicit query keys MUST be documented per app in the manifest.
  3. Manifest documents defaults; it does not override a loaded URL.

Invalid and unknown input

Unless an app documents strict mode, unknown query parameters MUST be ignored (forward compatibility). Invalid values SHOULD fall back to defaults; apps MAY log when edos.debug (or equivalent) is present—TODO: Verify per app before documenting edos.debug as shared.

Conformance checklist

  • [ ] Manifest is reachable at /.well-known/edos-integration.json on the app’s production origin over HTTPS.
  • [ ] Capsule or README lists bootstrap keys and where they are parsed (search vs hash).
  • [ ] Apps with shell sub-navigation publish navigation with per-view path routes (SPA fallback documented); optional edos.view alias only if needed; table presets use app-specific query keys per TABLE_GUIDANCE.md.
  • [ ] Journal / file streaming uses IPC.md where IFM applies; any additional embed postMessage types are listed in the manifest and do not collide with MessageTypeEnum strings unless coordinated.
  • [ ] Apps that consume another app’s origin-local data in a composition layout use the host proxy (edac.* today)—not hidden iframes to the provider origin.
  • [ ] Third-party APIs, assets, or frameworks shown in the UI have link attributions (bottom center, new-tab links) per DESIGN_GUIDANCE.md — Third-party service attribution, unless embedded mode documents a host-provided credit.

Status in this workspace (truth in code)

App edosEmbedded / embedding (summary) Manifest at /.well-known/…
Surface map (elite-dangerous-surface-map) Reads edosEmbedded from pre-hash location.search or from the hash query (#/…?edosEmbedded=…). Truthy values today: 1, true — see elite-dangerous-surface-map/ui/src/composables/useEdosEmbeddedMode.ts. TODO: publish manifest (TODO.md).
EDJEV (elite-dangerous-journal-event-viewer) Reads search params: edosEmbedded truey values true, 1, yes; alias mode=embedded; opt-out edosEmbedded=false. Also treats iframe context as embedded when not opted out — see elite-dangerous-journal-event-viewer/src/App.tsx. TODO: publish manifest (TODO.md).

Other hosted apps SHOULD align with the same manifest path and naming rules when they gain embed modes.

Optional discovery registries

Composition hosts and dashboards MAY consume federated, link-only registry indexes that point at each app’s entryUrl and integration manifest. That layer is defined in APP_DISCOVERY_REGISTRY.md.

Normative rule: Capability truth always comes from the manifest on the app origin (and optional runtime handshake per this document). Registry entries MUST NOT replace or embed manifests in link-only v1. External developers MUST NOT need any registry to integrate.

Changelog (this document only)

Revision Summary
Initial Introduced split from IPC.md; manifest path; optional handshake; transitional edosEmbedded note; workspace status table.
2026-05 In-app navigation views: path-first routes in manifest navigation, optional edos.view query alias, optional edos.navigation.* postMessage; distinction from table views (TABLE_GUIDANCE.md).
2026-05 Optional federated discovery registries cross-linked; manifest-at-origin remains normative (APP_DISCOVERY_REGISTRY.md).
2026-05 Composition host and sibling iframes: normative proxy pattern (edac.places.queryedos.places.query); forbidden hidden-iframe cross-app access.