API & the SDK
Every table and feature is reachable over a REST API under /api/*. From the frontend you use bm, a small typed client that is regenerated from your schema on every push — so bm.table('typo') is a TypeScript error, not a runtime surprise.
The typed client
Import the SDK and your generated types from 'bm'. The types come from src/bm.d.ts, which is rewritten automatically whenever you push a change to schema.prisma, app.yaml, flows.yaml, workflows.yaml, hooks.yaml, or your i18n files. Never edit it by hand.
import bm, { type User, type Contact } from 'bm';
const me: User | null = await bm.auth.me();
if (!me) { location.href = '/login.html'; return; }
// Strongly typed CRUD — list, get, create, update, delete.
const contacts: Contact[] = await bm.table('contacts').list({ limit: 50 });
const created = await bm.table('contacts').create({ name: 'Ada', email: '[email protected]' });
await bm.table('contacts').update(created.id, { status: 'won' });
await bm.table('contacts').delete(created.id);
What's on bm
| Surface | What it does |
|---|---|
bm.table(name) | Typed CRUD for a table: list, get, create, update, delete, count; restore on soft-delete tables; list({q}) on full-text tables; versions / revertTo on versioned tables. |
bm.auth | me, signIn, signUp, signOut, refreshMe — session lifecycle. |
bm.live(table, cb) | Subscribe to real-time changes for a table over SSE. |
bm.room(name) | WebSocket room: broadcast, presence, peer-to-peer signaling. |
bm.flows.<name>() | Call a custom route from flows.yaml, typed from its inputs. |
bm.workflow(table, id) | transitionTo, available, current for state machines. |
bm.notifications | In-app inbox: list, markRead, markAllRead, onNew. |
bm.upload(file) | Multipart file upload; returns a stored URL. |
bm.jobs | status / wait — poll async-flow jobs. |
bm.permissions | Per-row sharing: share, revoke, list. |
bm.api.{get,post,patch,delete} | Raw escape hatch — auto-CSRF and auto-JSON for any endpoint. |
The raw API
You don't have to use the SDK — any HTTP client works. The contract is plain REST and JSON. The one rule for cookie-authenticated requests is CSRF: send the token from the auto-injected <meta name="csrf-token"> tag as an X-CSRF-Token header on every mutation. (Bearer-token clients are exempt — see Auth & sessions.)
const csrf = document.querySelector('meta[name="csrf-token"]').content;
await fetch('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
credentials: 'same-origin',
body: JSON.stringify({ name: 'Ada', email: '[email protected]' }),
});
Errors
Every error is JSON: { error, message?, fields?, hint? }. Switch on the machine-readable error code and show message to the user. Common codes: 401 authentication required, 403 invalid CSRF token, 404 not found, 409 row changed since last read, 422 validation failed (with a fields map), 429 rate limited.