Skip to content
Home

Unbound Connect JS Loader

Status: Draft Last Updated: 2026-06-15 Audience: customer

The JS loader lets a web app open the Unbound Connect flow in-pageUnbound.connect({ token, onSuccess }), the Plaid Link interaction model — without shipping any Unbound UI into your bundle. The loader is a tiny, dependency-free script served from the LifeDB binary; it opens the hosted flow in a popup and relays a small versioned event stream back to your page over postMessage. Flow changes never require a loader update.

OAuth providers refuse to render inside iframes, so the flow runs in a popup (a top-level window), not an embedded frame. When the popup is blocked (typically mobile web), the loader falls back to a full-page redirect, completing through the identical redirect_uri contract.

Embedding the loader

Vendor the loader with a single <script> tag. The ?v=1 pins the major version (see Versioning).

<script src="https://<your-lifedb-host>/connect/v1/connect.js?v=1"></script>

Then call Unbound.connect when the user clicks your “Connect your data” button. The example below uses obviously synthetic values:

<button id="connect-btn">Connect your data</button>
<script>
document.getElementById("connect-btn").addEventListener("click", async () => {
// 1. Mint a connect token on YOUR backend (never in the browser) and hand it
// to the page. The token is short-TTL and single-use.
const token = await fetch("/api/unbound/connect-token", { method: "POST" })
.then((r) => r.json())
.then((d) => d.connect_token);
Unbound.connect({
token,
onSuccess: ({ code, state }) => {
// 2. POST the code to YOUR backend for exchange (see below). Never
// exchange in the browser — the exchange needs your client secret.
fetch("/api/unbound/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, state }),
});
},
onExit: ({ reason }) => {
// The user closed the popup or the flow ended without success.
console.log("connect exited:", reason); // "closed" | "error" | flow reason
},
onEvent: ({ name, payload }) => {
// Optional telemetry: the full relayed event stream (see the table below).
console.log("connect event:", name, payload);
},
});
});
</script>

token is required; onSuccess, onExit, and onEvent are all optional (a missing callback is a no-op).

Exchanging the code on your backend

onSuccess delivers { code, state } — exactly what redirect mode would deliver to your redirect_uri for the same journey. The code is a one-time authorization code; the loader never persists it and it never appears in a URL. Exchange it on your backend using your client credentials:

POST https://<your-lifedb-host>/connect/token
{ "code": "<code>", "client_id": "<id>", "client_secret": "<secret>" }

Verify that state matches the value you minted the session with before trusting the result — this is your CSRF guard, identical to redirect mode.

Event vocabulary

The flow page emits a small, versioned event stream. Every event reaches onEvent; the terminal ones also drive onSuccess / onExit. The vocabulary is additive-only within v1 — an old loader relays an unknown future event through onEvent untouched, so new events never break an existing embed.

EventWhenPayloadDrives
openThe flow page has loaded in the popup.{}onEvent
source_connectedA source’s grant landed during the journey (e.g. the Mac sync grant).{ source }onEvent
successThe journey completed; the authorization code is ready.{ code, state }onEvent + onSuccess
exitThe flow ended without success (a flow-side exit reason).{ reason }onEvent + onExit
errorThe journey hit a recoverable client-side failure.{ reason }onEvent + onExit({error})

Notes:

  • success is terminal. After it fires, the popup closes and onSuccess receives { code, state } exactly once.
  • Closing the popup before a terminal event fires onExit({ reason: "closed" }). There is no hung promise; the underlying session simply expires by its TTL.
  • error is surfaced to onExit({ reason: "error" }). The flow may still let the user retry in place — error is the embedder’s signal, not a forced teardown.
  • Treat any event name you do not recognize as forward-compatible telemetry: log it via onEvent and ignore it. Do not branch your completion logic on anything but success.

If the browser blocks the popup (common on mobile web), the loader navigates the current page to the hosted flow as a full-page redirect instead. The journey then completes through your registered redirect_uri — the same completion contract as redirect mode — so your backend’s redirect handler must remain wired even when you use the loader. No code path is lost; only the in-page feel is.

Security model

  • Origin pinning, both directions. The loader accepts postMessage events only from the flow’s origin and only those tagged as Unbound Connect messages; everything else is ignored. The flow, in turn, posts events only to the origin of your registered redirect_uri — never a wildcard, and never an origin supplied by an incoming message. The two pinned origins are the same one the relay posts the completion to.
  • No code persistence. The loader transmits only the connect token and the event stream. The authorization code rides in the event payload to your onSuccess and is never written to storage, a cookie, or a URL.
  • No secrets in the browser. Mint the connect token and exchange the code on your backend. The loader carries no client secret and embeds none.

These origin-pinning and no-persistence guarantees — along with the migration-free / cookie-free / code-never-in-URL invariants — are part of the loader’s contract and hold across flow updates.

Versioning

The loader is pinned by major version via the query string: connect.js?v=1. Its contract is the event vocabulary only — within v1 the vocabulary only grows, so a loader pinned to ?v=1 keeps working as the flow evolves. A breaking change to the vocabulary or session semantics would ship as connect.js?v=2 under a /connect/v2/ flow; v1 embeds are unaffected. The same loader bytes are served regardless of the ?v value (it is a cache-buster), so pinning the major is what matters.

  • Connect overview — the hosted flow this loader opens, the delivery modes, and the versioning contract; see § JS loader
  • Webhooks — the push channel for grant transitions that complete after the user has left the flow
  • Authentication — how your backend authenticates to mint the connect token
  • Authorization flow — the backend token-mint and code-exchange the loader hands off to

Code References

  • connect-starter-kit — copyable BFF + example front-end implementing the token-mint / code-exchange / redirect_uri contract this guide describes.