braid · @braidlabs/* Pin it next to your monitor Reference card · edition 1

The cheatsheet.

Everything you'll reach for, in the order you'll reach for it. Short snippets, real APIs, no filler.

1

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
2

Compat vs contract

Two ways to start an app. Pick by one question: can you change the app?

Question compat contract
Forapps you can't changeapps written for Braid
Code changesnoneone defineFragment call
The app seesa convincing window, document, historyan env object, nothing faked
Starts froma server URLa blob: URL, in memory
Needs the gatewayyesno, 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.

3

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.

4

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.

5

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