The cheatsheet.
Everything you'll reach for, in the order you'll reach for it. Short snippets, real APIs, no filler.
Setup & the gateway
The gateway is a library, not a service. It runs inside the server you already have.
Install
$ npm i @braidlabs/gateway @braidlabs/core
Mount it first, before your app's routes
import { createGateway } from '@braidlabs/gateway'; import { toNodeMiddleware } from '@braidlabs/gateway/node'; const gateway = createGateway({ mode: 'production', registry: [{ id: 'billing', // its name everywhere endpoint: 'http://localhost:4201', // its server pierce: ['/billing', '/billing/*'], // pages it's on timeoutMs: 1200, }], }); app.use(toNodeMiddleware(gateway)); // Express, Nest, Vite app.use(hostAppRouter);
On Workers, Deno, Bun or Nitro
export default toFetchHandler(gateway, appHandler);
In the browser, and in your HTML
import { initBraid } from '@braidlabs/core'; initBraid(); // defines <fragment-slot> <fragment-slot name="billing"> <p slot="fallback">Billing is unavailable.</p> </fragment-slot>
The fallback shows only if the app errors or goes away.
A widget on every page (not tied to the URL)
{ id: 'notifications', bound: false,
src: '/panel', pierce: ['/', '/*'] }
Check it worked
# content is in the first response curl -s localhost:3000/billing | grep -c fragment-slot # which apps the gateway put on the page curl -sI localhost:3000/billing | grep x-braid-fragment-id
The URLs you'll see
- /__braid/realm/:id/
- tiny page each app's sealed space starts from
- /__braid/doc/:id/…
- the app's HTML, when the server didn't add it
- /__braid/frag/:id/*
- the app's own files and API calls, forwarded
Compat vs contract
Two ways to start an app. Pick by one question: can you change the app?
| compat | contract | |
|---|---|---|
| For | apps you can't change | apps written for Braid |
| Code changes | none | one defineFragment call |
| The app sees | a convincing window, document, history | an env object, nothing faked |
| Starts from | a server URL | a blob: URL, in memory |
| Needs the gateway | yes | no, any static host |
Compat: nothing to write
It's the default. Register the app with the gateway (section 1) and serve it exactly as you do today.
Contract: the page
<fragment-slot name="checkout" adapter="contract" entry="https://checkout.example.com/main.js"> </fragment-slot>
Contract: the app
import { defineFragment } from '@braidlabs/core'; defineFragment({ contract: { version: '2.1.0', requires: { host: '>=1.4.0' } }, mount(env) { const app = createApp(env.root, { basePath: env.location.basePath, }); env.signal.addEventListener('abort', () => app.destroy()); }, });
Contract, in React
import { defineReactFragment } from '@braidlabs/react'; defineReactFragment({ render: (env) => <Checkout cart={env.props.cart} />, });
What's on env
env.root // draw here env.location // your route, incl. basePath env.history // push · replace · back · onChange env.props // from the host · onPropsChanged() env.context // shared values · get · subscribe env.emit() // tell the host something env.setDirty() // "unsaved work here" env.onClosing() // last chance to save env.signal // aborts when you're removed
Code from outside your company
<fragment-slot name="chat" trust="untrusted" src="https://widget.vendor.com/"></fragment-slot>
A real cross-origin wall: own cookies, own storage, own document. It no longer flows with your page.
Talking between apps
Four options, from simplest to strongest. Start at the top.
A · Props: host → app
// host, plain DOM slot.props = { accountId: 'ACC-99201', currency: 'USD' }; // host, React <BraidFragment name="billing" props={…} /> // app env.onPropsChanged((props) => render(props));
B · Events: app → host
// app env.emit('invoice:paid', { invoiceId: 'INV-101' }); // host, plain DOM: bubbles from the slot slot.addEventListener('braid:event', (e) => { const { type, detail } = e.detail; }); // host, React · Angular <BraidFragment onFragmentEvent={onEvent} /> <braid-fragment (fragmentEvent)="onEvent($event)" />
C · Context: shared values everyone can read
import { braidContext, initBraid } from '@braidlabs/core'; // host: say who may read what initBraid({ capabilities: { billing: { context: { read: ['user'] } }, } }); braidContext.set('user', { id: 'u-1', locale: 'en-GB' }); // app env.context.subscribe('user', (user) => greet(user));
D · The event bus: across apps, tabs and reloads
import { createEventBus } from '@braidlabs/data'; const bus = createEventBus({ consumer: 'billing', driver }); const cart = bus.channel('cart'); // state: "this is now true" (late joiners get it) cart.broadcast({ items: 3 }); cart.subscribe((c) => badge(c.items)); // events: "this happened" await cart.emit('item:added', { sku: 'A1' }, { delivery: 'at-least-once' }); // survives a reload cart.addEventListener('item:added', (e) => log(e.payload)); // intents: "someone, please do this" bus.addIntentListener('ViewInvoice', { handler: open }); await bus.raiseIntent('ViewInvoice', { id: 'INV-101' });
Rule of thumb: state is a noun, an event is a verb. Don't use one for the other.
Skew protection
Skew is when two builds meet and disagree: an old tab and a new deploy, an old draft and new code, an old app and a new API.
Version your data
import { versioned } from '@braidlabs/skew'; // frozen snapshots, never your live types interface V1 { id: string; body: string } interface V2 { id: string; title: string; body: string } export const Draft = versioned<V1>('draft') .next<V2>('lift first line into a title', (p) => ({ ...p, title: p.body.split('\n')[0] ?? '', }));
Read it back, safely
const r = Draft.read(raw); if (r.ok) render(r.value); // always the current shape else switch (r.reason) { case 'ahead': // written by a NEWER build case 'gap': // a migration step is missing case 'invalid': // not this kind of data case 'threw': // a migration failed case 'retired': // older than you still support }
Data with no version label counts as v1, so you can adopt this without a backfill.
Stamp every build
$ npx skew-stamp --out src/generated/build-id.ts \
--manifest dist/skew-manifest.json
Old tab, new deploy
// Angular: recover from missing lazy chunks provideSkewRecovery({ identity: BUILD_IDENTITY, manifestUrl: '/skew-manifest.json', }); // any framework: keep old chunks reachable createGateway({ registry, serviceWorker: true }); await registerBraidServiceWorker({ buildId: BUILD_ID });
Old app, new API
const resolver = createContractResolver(); const r = await resolver.readResolving(FundV1, body, wellKnownContractUrl(API, 'portfolio-fund')); // on 'ahead', it learns the new steps from the API // and gives you a labeled, older-shape copy
The one rule: a migration must never import your live types. Each step closes over its own frozen snapshot.
Data, offline & the outbox
Saved data, cached server data and queued writes all go through the same versioned envelope, so none of it breaks when your app's shapes change.
Pick a driver: where the data lives
import { indexedDbRecordDriver, // durable, survives reloads sessionStorageRecordDriver, // this tab only memoryRecordDriver, // tests, and throwaway state } from '@braidlabs/data'; const driver = indexedDbRecordDriver({ database: 'my-app', collections: ['notes', 'outbox'], // declare all up front });
State you own: the record store
const notes = createRecordStore({ driver, collection: 'notes', schema: NoteContract, }); await notes.put({ id: 'n1', partition: 'default', value: { id: 'n1', title: 'Buy milk' } }); const note = await notes.get('n1', 'default');
Server state: the data client
const data = createDataClient({ driver, partition: () => 'default', collection: 'notes', }); const note = data.query({ key: 'note:n1', tags: ['note#n1', 'notes'], schema: NoteContract, fetch: (signal) => fetch('/api/notes/n1', { signal }) .then((r) => r.json()), }); note.subscribe((s) => { if (s.status === 'ready') render(s.data); }); data.invalidate('note#n1'); // refetch everything tagged
Live updates: push into the cache
client.connect({ schema: Quote, source: (sink, signal) => {
const ws = new WebSocket('wss://…/quotes');
ws.onmessage = (e) => sink.receive({ key: `quote:…`, value: e.data });
signal.addEventListener('abort', () => ws.close());
} });
Writes that survive going offline
const data = createDataClient({ driver, partition: () => 'default', outbox: createOutbox({ driver, owner: 'notes-app' }), }); // register at startup, so a write queued before a reload // still has something to replay it data.registerMutation('note.rename', sendRename, { tags: ['note#n1'] }); const out = await data.mutate({ key: 'note:n1', schema: NoteContract, mutationId: 'note.rename', // the kind of write input: { id: 'n1', title: 'Buy oat milk' }, patch: { title: 'Buy oat milk' }, // shown right away tags: ['note#n1'], send: sendRename, }); out.status; // 'confirmed' landed · 'queued' waiting to retry
Did the data come from an older or newer app?
note.subscribe((s) => {
if (s.status !== 'ready') return;
s.derivedPaths; // filled in by a migration
s.downgradedFrom; // written by a newer app
s.lossyPaths; // fields you can't see
});
Several tabs, one queue
await withLock('flush-outbox:tenant_acme', () => flush()); // only one tab or app runs this at a time
Signed-in users and tenants
const tenancy = createTenancy({ driver, collections: ['notes', 'outbox'] }); await tenancy.signIn({ userId: 'u-1', actingAs: 'acme' }); createDataClient({ driver, partition: tenancy.partition }); await tenancy.signOut(); // wipes every partition for u-1
In Angular
provideSkewData({ owner: 'notes-app', persistOutbox: true,
database: 'my-app', collections: ['notes'] });
inject(OutboxService).register('note.rename', send);