How-toCode from a running demobeginner~15 minBuilt on the docs-site-gallery demo →
@hyperfrontend/features

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, which bundles the host SDK and declares no dependencies of its own. The snippets come from this site's demo gallery, a Next.js host embedding a separately deployed Vue clock.

1. Install the shell package

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> and runs a live message channel, so import it from code that only ever runs in the browser.

'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'

e.g. apps/docs-site/src/components/demos/demo-wiring.ts#L2-L6

Every generated shell exports createFeatureShell, 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.

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

e.g. apps/docs-site/src/components/demos/demo-wiring.ts#L147-L153

4. Arm the degradation path

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

// 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)
}

e.g. apps/docs-site/src/components/demos/demo-embed.tsx#L89-L96

5. Open the session and subscribe

Create the shell against a container element, subscribe, then open(). Sends queue until the handshake completes, so everything you learn about the session arrives through on.

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()

e.g. apps/docs-site/src/components/demos/demo-embed.tsx#L103-L135

Three calls are yours rather than the SDK's: ignore suspect 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' 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

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

e.g. apps/docs-site/src/components/demos/demo-embed.tsx#L142-L151

destroy() 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 the feature declared in its feature.config.ts. Compare the two in your build or deploy step: a counterpart that omits the protocol falls back to plaintext, and no runtime signal reports it.

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 at any page that is not a feature, and watch the handshake time out, the deadline fire, and your fallback hold the space.

Reference and background

Next steps