@narrative.io/app-bridge
v0.1.1
Published
Bridge between the Narrative platform and embedded third-party app UIs — origin-pinned MessageChannel protocol, zero runtime dependencies
Readme
@narrative.io/app-bridge
The bridge between the Narrative platform and an embedded third-party app UI.
An app registers the origin it is served from, the platform embeds it in a
sandboxed iframe, and this package carries authorization, navigation, and
context between the two over an origin-pinned MessageChannel.
- Zero runtime dependencies, and that is a hard rule rather than an aspiration. You bundle this package into your app, so every dependency it had would become supply-chain surface in your application.
- Runtime-validated on both sides. Types disappear when the code runs; every inbound message is checked structurally before it is acted on.
- No network client. The bridge is
postMessageand nothing else. You call the Narrative API with plainfetchand the bearer token it hands you. - Nothing fails silently. Unknown methods return errors, every request
resolves or rejects, and all failures are
BridgeErrors with stablecodes.
bun add @narrative.io/app-bridge # or npm / pnpm / yarnStability. Wire protocol version
1is settled — the message shapes in docs/protocol.md are what the platform speaks. The package stays on0.xwhile the first external integrations land, so a minor version may still adjust the JavaScript surface. Pin an exact version if that matters to you, and watch releases.
Quick start
import { connect } from '@narrative.io/app-bridge/guest'
// Pin the origins that may embed you. This is the security decision the
// whole package is built around — never use '*'.
const bridge = await connect({ platformOrigin: 'https://app.narrative.io' })
// Context arrives with the handshake, so there is no round-trip before your
// first render.
console.log(bridge.context.user, bridge.context.company, bridge.context.tier)
// Calling the Narrative API is plain fetch plus a bearer token. getToken()
// caches and renews automatically.
const { token } = await bridge.getToken()
const response = await fetch(`${bridge.context.apiBaseUrl}/datasets`, {
headers: { Authorization: `Bearer ${token}` },
})
// Navigation: your app is a controlled component. Tell the platform where the
// user went, and switch views when the platform answers.
bridge.pathChanged('/reports')
bridge.onNavigate((path) => showView(path))
// The platform tells you about live changes; you never poll.
bridge.onContextChange((context) => showCompany(context.company))
bridge.onSessionEnd((reason) => showSignedOutBanner(reason))No bundler? There is a <script> build that attaches a single global:
<script src="https://unpkg.com/@narrative.io/app-bridge/dist/app-bridge.global.js"></script>
<script>
NarrativeAppBridge.connect({ platformOrigin: 'https://app.narrative.io' }).then((bridge) => {
document.title = bridge.context.company.name
})
</script>Four rules that will save you an afternoon
- Call
connect()as early as possible — before your framework boots — so the handshake overlaps your app's own startup. - Your app must be frameable. An
X-Frame-Options: DENYheader, or a CSPframe-ancestorsdirective that excludes the platform, means your app cannot be embedded at all, no matter what the platform allows. - Look and feel is yours. The bridge deliberately carries no styling or theming information, and never will. It moves identity, authorization, and navigation — facts, not presentation.
- Client secrets never go in the browser. The token
getToken()hands you is installation-scoped and short-lived. Your own backend uses OAuth client credentials instead.
Documentation
| | |
|---|---|
| Getting started | Connecting, calling the API, navigation, and the full guest API reference |
| Protocol | The wire format, message by message, and the compatibility rules |
| Security model | What the bridge guarantees, what it does not, and what each side must do |
| Hosting apps | The host surface, for a platform embedding apps |
| Troubleshooting | Every BridgeError code and what actually causes it |
| Architecture | Why the protocol is shaped this way, including the paths not taken |
The surface, at a glance
One handshake, two methods, four events. Window-level traffic is exactly two
messages — hello from the app, handshake back with a transferred
MessagePort — and everything after that travels on the port, unreachable from
any other window.
| | name | direction |
|---|---|---|
| method | getToken | app → platform |
| method | getContext | app → platform |
| event | pathChanged | app → platform |
| event | navigate | platform → app |
| event | contextChanged | platform → app |
| event | sessionEnded | platform → app |
Three entry points, and no root export — importing the package bare resolves
nothing. That is deliberate: it makes it impossible for an app to accidentally
bundle host, which is the platform's side of the conversation.
| Subpath | For |
|---|---|
| @narrative.io/app-bridge/guest | An app being embedded. This is the one you want. |
| @narrative.io/app-bridge/host | A platform doing the embedding. |
| @narrative.io/app-bridge/protocol | Shared types and validators, if you need to name them. |
Support
Bugs and feature requests: GitHub issues. Security reports go to [email protected] — see SECURITY.md.
Contributing: CONTRIBUTING.md.
License
MIT © Narrative I/O, Inc.
