Frontend
The platform serves files from static/ and compiles TypeScript and TSX to JavaScript on the fly — no Node, no npm install, no build step you run. You edit a .tsx file and push; the browser loads the compiled .js.
Project layout
myapp/
├── schema.prisma # your data model
├── app.yaml # config: auth, access, roles, features
├── src/
│ └── bm.d.ts # auto-generated types — do NOT edit
└── static/
├── index.html # GET /
├── login.html # GET /login
├── app.tsx # your entry — edit this
└── styles.css
How it's served
GET /servesstatic/index.html;GET /reportsservesstatic/reports.html.GET /static/app.jsfindsstatic/app.tsx, compiles it with esbuild, and caches the output by file modification time.- Anything else that asks for HTML falls back to
index.html(single-page-app routing). - A CSRF token is injected into every served page as
<meta name="csrf-token">and into every POST form.
Prefer React, Preact, or HTMX? Import them from a CDN in your entry, or drop a build's output into static/. The platform treats static/ as opaque — the only contract is the API.
A minimal entry
import bm, { type Message } from 'bm';
async function boot() {
const me = await bm.auth.me();
if (!me) { location.href = '/login.html'; return; }
const render = (rows: Message[]) => {
document.getElementById('list')!.innerHTML =
rows.map(m => bm.html`<li>${m.body}</li>`).join('');
};
render(await bm.table('messages').list({ limit: 50 }));
// Re-fetch when anything changes (see Real-time).
bm.live('messages', async () => {
render(await bm.table('messages').list({ limit: 50 }));
});
}
boot();
bm.html is an XSS-safe tagged template — interpolated values are escaped in both text and attribute positions, so you don't hand-roll escaping. Wrap an already-safe string in bm.raw(...) to opt out per value.
Styling
Tailwind is wired into every page out of the box — utility classes work with no build step. Reserve styles.css for what utilities can't express (keyframes, @font-face, third-party overrides). The scaffold ships a small components.tsx with primitives like Button and Card; when a class string shows up a second time, move it into a shared primitive.
Sharing chrome across pages
Don't copy the <head>, nav, and footer into every page. Factor them into static/partials/ and pull them in — one source of truth, edited once.
<head>
<include src="partials/head.html" />
<title>Reports</title>
</head>
A couple of gotchas
- A
POSTreturns the full inserted row — use it directly instead of re-fetching. - A real-time event carries only
{table, action}, never the row — re-fetch on the event. - A nullable
DateTimeserializes as JSONnull; guard before formatting it. - Cache-busting is automatic: a push rewrites script URLs so browsers always get fresh code. Don't add your own
?v=query string.
Next: API & the SDK for the full client surface, or Real-time for live updates.