Application Contract
Framework-neutral application protocol, lifecycle ownership, readiness, services, and compatibility rules for Fronts 2.0.
Protocol identity
Every producer exports a FrontsApp created by defineApp(), defineReactApp(), or
defineVueApp(). The helper adds a non-enumerable symbol brand plus enumerable protocol name and
version fields. The symbol is a local marker; the cross-package ABI is the structural mount shape
plus the protocol name and major version. The host validates that ABI, and a symbol brand never
bypasses protocol-version validation.
The exposed Module Federation module should normally have this shape:
export interface FrontsAppModule<Props, Services extends object> {
readonly default: FrontsApp<Props, Services>;
}The default expose is ./application; callers can choose another expose in AppRef.
Mount input
interface MountContext<Props, Services extends object> {
readonly identity: {
readonly instanceId: string;
readonly name: string;
readonly version?: string;
readonly expose?: string;
readonly traceId?: string;
};
readonly props: Readonly<Props>;
readonly services: Readonly<Services>;
readonly signal: AbortSignal;
readonly target: Element | ShadowRoot;
}Invariants:
identity.instanceIdis unique while its mount transaction or retained instance record exists.- the
propsreference is captured when the mount transaction starts, but Fronts does not clone or freeze the caller's object. Callers and producers must treat it as read-only shared input. servicescontains only declared and authorized capabilities.signalaborts when mounting is cancelled or the instance is force-aborted.targetis supplied and controlled by the selected container for the lifetime of this mount.- a producer must render only inside its target unless a declared platform capability explicitly permits another effect.
Mount output
interface AppHandle<Props> {
readonly ready?: PromiseLike<void>;
update?(props: Readonly<Props>): void | PromiseLike<void>;
activate?(): void | PromiseLike<void>;
deactivate?(): void | PromiseLike<void>;
unmount(): void | PromiseLike<void>;
}ready
mount() means resources were accepted; ready means the application committed its usable UI.
If omitted, the application is ready as soon as mount() returns. Hosts impose a deadline.
Readiness must reject on initial render failure. It must not wait for optional analytics, background data, or an interaction that may never happen. Replacement commits only after readiness succeeds.
update(props)
Updates an existing mounted application without changing identity, deployment, target, or service scope. Implementations should resolve after the framework commits the update. When unsupported, the host reports a typed lifecycle failure instead of silently remounting.
activate() and deactivate()
Optional visibility/activity hints. They are not mount/unmount and must preserve application state. Typical uses are pausing timers, subscriptions, media, or expensive rendering while a tab is inactive.
unmount()
Required and treated as idempotent by the host boundary. It must remove render roots, listeners, timers, observers, subscriptions, and producer-owned DOM. It must not remove the container element; the container adapter owns that resource.
State machine
resolving → loading → preparing → mounting → ready
│
┌──────── updating ◄────────┤
├──────── active ◄────────┤
└──────── inactive ◄────────┘
│
▼
unmounting → destroyed
Any pending phase ── failure/abort + rollback ──► failed | aborted (terminal)Operations are serialized per instance. Calls that are invalid for the current state reject with
a FrontsError; they are never silently queued across destruction. Clean failed/aborted
transactions release their host record after rollback; incomplete cleanup remains visible to
host.audit() without inventing a later destroyed transition.
Cancellation
An external mount signal is combined with an instance-owned signal. Aborting either prevents later phases from starting and triggers rollback. Loaders and resolvers receive the signal and should pass it to network requests. Application code must listen when it starts long-running work.
Aborting is cooperative: JavaScript already evaluated in the same realm cannot be forcibly stopped. Stronger runtime isolation requires an iframe and ultimately destroying that child realm.
Capabilities
Applications declare string keys:
export default defineApp<Props, HostServices>({
capabilities: ['navigation', 'telemetry'],
// ...
});Declaration is necessary but not sufficient. The host's capabilityPolicy evaluates each key for
the concrete application identity and receives the instance signal. The runtime stops awaiting
asynchronous authorization when that signal aborts. A declared capability that is unavailable or
denied rejects an ordinary application mount; undeclared services never enter its facade. A service
scope is revoked on cleanup, so retained method proxies fail rather than continuing to affect the
host.
Static services remain the simplest host configuration. When a capability must be bound to one
application instance, use createServices as an instance-level provider:
const host = createFrontsHost({
// resolver, loader, static services ...
createServices({ identity, operation, resolved, resolution, capabilities, signal }) {
const channel = telemetry.open({
app: identity.name,
instanceId: identity.instanceId,
tenant: resolution.tenant,
traceId: operation.traceId,
version: resolved.version,
signal,
});
return {
services: { telemetry: channel },
dispose: () => channel.close(),
};
},
});The provider runs once per mount/replace transaction after resolution and application validation.
Its frozen capabilities list contains declarations, not grants. Provider services override static
services with the same key; the host then applies availability checks and capabilityPolicy, and
only the requested, authorized keys enter the application facade.
Returning the result transfers ownership to the host. dispose is called at most once after
facade revocation, including authorization failure, aborted mount, ordinary unmount, and host
shutdown. It may be asynchronous; a failure is surfaced as an unmount cleanup failure without
skipping container disposal. An aborted mount stops awaiting a pending provider; if its result
arrives later, the host disposes it and keeps that cleanup visible as a pending operation. Providers
must still honor signal to stop work promptly. If a provider throws before returning its result,
it still owns rollback of any partially allocated resources.
For iframe applications, IframeProxyLoaderOptions.capabilities is a host-controlled maximum,
not a list of mandatory dependencies and not the final child grant. Entries unavailable from the
instance service provider or denied by policy are omitted from the parent offer rather than failing
the proxy before the child is known. After loading the real child application, the agent negotiates
its declared subset with the parent. Only the exact negotiated intersection becomes RPC stubs, and
the parent checks that grant on every call. A child is still rejected when one of its own declared
requirements is absent from that offer.
Prefer narrow intent APIs such as navigation.go() and telemetry.emit() over ambient objects
such as window, a Redux store, or a generic message bus.
Framework adapters
React
defineReactApp() owns a React root, context, error boundary, commit-based readiness, prop updates,
optional Strict Mode, and root unmount. Host services and identity are available through hooks.
Vue 3
defineVueApp() owns a Vue app, provided context, reactive prop updates, nextTick() readiness,
configuration hook, and app unmount. It creates an inner element when mounting into Shadow DOM.
These adapters intentionally expose fewer framework-specific features than the official MF Bridge. Use official Bridge directly when its routing/data/SSR integration is the actual requirement; use Fronts adapters when the application must participate in the Fronts host ABI.
Compatibility rule
Protocol major versions are strict. A host must reject an unknown major rather than trying to guess behavior. New optional handle methods can be added compatibly; changing mount semantics, readiness, service authorization, or cleanup ownership requires a new protocol major.
Verification
pnpm exec vitest run packages/core/test/app.test.ts packages/core/test/host.test.tsverifies protocol branding, application validation, lifecycle ordering, readiness, and cleanup.pnpm exec vitest run packages/core/test/services.test.ts packages/core/test/iframe.test.tsverifies capability scoping, provider cleanup, and the negotiated iframe service contract.packages/core/src/appis the public protocol source of truth.packages/react/testandpackages/vue3/testverify that framework producers implement the same contract.