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¶
Rules:
- The consumer posts only to
window.parent(the composition host), using a host-specific proxytype(todayedac.places.query). - The host validates
event.originagainst slot URLs in the active composition, resolves a provider slot, and forwards the provider’s documented querytypeto that iframe’scontentWindow. - The provider answers on the host
window; the host correlatesrequestIdand forwards the provider result to the consumer iframe (postMessagewith the consumer’s origin astargetOrigin). - The provider slot must be mounted (page slot, overlay, or
backgroundSlots[]in EDAC)—visibility is not required. - 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:
contractMUST 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 acontractrevision. - Transitional key: Today several apps implement
edosEmbedded(no dot) for “embedded / chrome-reduced” behavior. New shared keys SHOULD use theedos.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. |
Manifest: navigation (recommended)¶
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:
viewIdMUST match a manifestnavigation.views[].idor be ignored.- The app SHOULD navigate to that view’s
path(and update history) when the view changes — user action, host message, or query alias resolution. - The app MAY emit
edos.navigation.changedafter user-driven switches so hosts can sync surrounding chrome. - Types MUST NOT collide with
MessageTypeEnumstrings unless coordinated via IPC.md and@howfe/inter-frame-messenger. - 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. |
Manifest — recommended fields¶
| 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:
- Use a dedicated
typestring (for exampleedos.handshake) in a small JSON payload; this is not part ofMessageTypeEnumunless maintainers deliberately add it to@howfe/inter-frame-messenger(then IPC.md and all consumers MUST be updated in lockstep). - Payload SHOULD include at least:
appId,contract,buildId(or semver). - 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:
- Explicit query parameters override profile defaults unless a key’s definition says otherwise.
- 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.
- 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.jsonon 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
navigationwith per-viewpathroutes (SPA fallback documented); optionaledos.viewalias 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
postMessagetypes are listed in the manifest and do not collide withMessageTypeEnumstrings 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.query → edos.places.query); forbidden hidden-iframe cross-app access. |