# How to embed a feature someone else shipped

Another team's app runs inside your page, tells you honestly whether it is alive, and gives way to your own fallback when it is not.

Their app is not yours to learn. It arrives as a package, ships on its own schedule, and can be down while your page is up, so what you build is a contract and a liveness judgement.

You install one thing: the shell package their build produced with [`@hyperfrontend/features`](https://www.hyperfrontend.dev/docs/libraries/features.md), which bundles the host SDK and declares no dependencies of its own. The snippets come from this site's [demo gallery](https://www.hyperfrontend.dev/demos/), a Next.js host embedding a separately deployed Vue clock.

## 1. Install the shell package

```bash
npm install @acme/checkout-feature-shell
```

A tarball the team sent you installs the same way: `npm install file:vendor/acme-checkout-feature-shell-1.0.0.tgz`.

## 2. Keep the import in browser-only code

A shell mounts an [`<iframe>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) and runs a live message channel, so import it from code that only ever runs in the browser.

```ts
'use client'

import { createFeatureShell as createClockShell } from '@hyperfrontend/demo-clock-shell'
import { createFeatureShell as createHeartbeatShell } from '@hyperfrontend/demo-heartbeat-shell'
import { createFeatureShell as createKoiPondShell } from '@hyperfrontend/demo-koi-pond-shell'
```

<sub>e.g. [apps/docs-site/src/components/demos/demo-wiring.ts#L2-L6](https://github.com/AndrewRedican/hyperfrontend/blob/main/apps/docs-site/src/components/demos/demo-wiring.ts#L2-L6)</sub>

Every generated shell exports [`createFeatureShell`](https://www.hyperfrontend.dev/docs/libraries/features/architecture/#shell-generation), so alias at the import when you host more than one.

## 3. Decide what "alive" means

Per feature, record how to create its shell, which contract events prove it is rendering, and how much silence you will tolerate before writing the session off. Ask the feature's team which event fires soonest and what silence means for their app.

```ts
clock: {
  createShell: (options) => createClockShell(options),
  contractLabel: 'contract 0.3.0 · protocol v3',
  // why: The clock streams a tick at 1 Hz from open, so the first tick proves the app renders.
  proofEvents: ['tick'],
  silenceTimeoutMs: 6000,
},
```

<sub>e.g. [apps/docs-site/src/components/demos/demo-wiring.ts#L147-L153](https://github.com/AndrewRedican/hyperfrontend/blob/main/apps/docs-site/src/components/demos/demo-wiring.ts#L147-L153)</sub>

## 4. Arm the degradation path

One deadline, armed before you open and pushed out on every sign of life.

```tsx
// how: One re-arming deadline serves connect-timeout and mid-session death alike — every proof of life pushes it out, so only real silence fires it.
let deadline: ReturnType<typeof setTimeout> | null = null
const armDeadline = () => {
  if (deadline !== null) {
    clearTimeout(deadline)
  }
  deadline = setTimeout(() => apply('offline'), wiring.silenceTimeoutMs)
}
```

<sub>e.g. [apps/docs-site/src/components/demos/demo-embed.tsx#L95-L102](https://github.com/AndrewRedican/hyperfrontend/blob/main/apps/docs-site/src/components/demos/demo-embed.tsx#L95-L102)</sub>

## 5. Open the session and subscribe

Create the shell against a [`container`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-container) element, subscribe, then [`open()`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle). Sends queue until the handshake completes, so everything you learn about the session arrives through `on`.

```tsx
const shell = wiring.createShell({ container: element, url: featureUrl })
const subscriptions = [
  // why: A proof event is post-open product traffic — the app is rendering, so the crossfade never reveals a blank frame.
  ...wiring.proofEvents.map((proof) =>
    shell.on(proof, () => {
      armDeadline()
      apply('live')
    })
  ),
  // why: The status payload is the watchdog snapshot object, not a bare state string.
  // why: `suspect` alone never demotes — a session whose product traffic still flows is visibly alive, and demoting on it makes the embed blink out for one beat interval before the next proof event restores it.
  shell.on('status', (data) => {
    const state = isRecord(data) ? data['state'] : undefined
    if (state === 'healthy') {
      armDeadline()
      apply('live')
    } else if (state === 'gone') {
      apply('offline')
    }
  }),
  // why: A close mid-session is usually a feature reload; the SDK re-adopts the new document, so report the honest in-between state and re-arm.
  shell.on('close', () => {
    apply('connecting')
    armDeadline()
  }),
  shell.on('error', (data) => {
    if (isRecord(data) && data['reason'] === 'open-timeout') {
      apply('offline')
    }
  }),
]
armDeadline()
shell.open()
```

<sub>e.g. [apps/docs-site/src/components/demos/demo-embed.tsx#L109-L141](https://github.com/AndrewRedican/hyperfrontend/blob/main/apps/docs-site/src/components/demos/demo-embed.tsx#L109-L141)</sub>

Three calls are yours rather than the SDK's: ignore [`suspect`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-HeartbeatState) while product traffic still arrives, report a mid-session `close` as connecting rather than offline (it is usually the feature reloading), and treat `error` with [`reason: 'open-timeout'`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api) as terminal.

## 6. Reveal the frame on the first proof event

Render your fallback under the iframe and swap to the frame on the first proof event, so a dead origin shows your artwork instead of a browser error page.

## 7. Close politely

```tsx
return () => {
  disposed = true
  if (deadline !== null) {
    clearTimeout(deadline)
  }
  detachEffects?.()
  subscriptions.forEach((unsubscribe) => unsubscribe())
  notifyShell.current?.(null)
  shell.destroy()
}
```

<sub>e.g. [apps/docs-site/src/components/demos/demo-embed.tsx#L201-L210](https://github.com/AndrewRedican/hyperfrontend/blob/main/apps/docs-site/src/components/demos/demo-embed.tsx#L201-L210)</sub>

[`destroy()`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) releases the DOM along with the channel. Use `close()` when the feature needs a flush window first.

## 8. Compare the protocol pin on both sides

A shell bakes the [protocol](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SecurityProtocol) the feature declared in its [`feature.config.ts`](https://www.hyperfrontend.dev/docs/libraries/features/cli/#config-resolution), and the session is fail-closed: a counterpart that cannot run that protocol is denied, [`error`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api) fires with `reason: 'security-unavailable'`, and the host tears the mount down. Compare the two pins in your build or deploy step so the mismatch never reaches a user. For `v4`, give both sides the same [`sharedKey`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-sharedKey) of at least 16 characters: with different keys no frame ever authenticates, and the session closes with `reason: 'security-unconfirmed'`.

## Check it worked

Your fallback gives way to the live frame on the first proof event. To rehearse the failure path, point the shell's [`url`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-url) at any page that is not a feature, and watch the handshake time out, the deadline fire, and your fallback hold the space.

---

Canonical page: https://www.hyperfrontend.dev/docs/guides/embed-a-shipped-feature/
This file: https://www.hyperfrontend.dev/docs/guides/embed-a-shipped-feature.md
Documentation index: https://www.hyperfrontend.dev/llms.txt
