Custom blocks
When the standard components cannot show something, a maker can add a custom block: their own HTML and script, run in a sandbox with no network, fed only by the queries, aggregates and actions it declares. The host runs those as the person viewing the page, so a block can never see or change more than that person could.
The sandbox
- The block renders in an iframe with an opaque origin and
sandbox="allow-scripts". It cannot read Renly's cookies or storage. - Its content security policy is the one Sites use: no network (
connect-src 'none'), with scripts and styles allowed only from cdnjs, jsDelivr, unpkg and the Tailwind CDN, and images from https hosts. - The frame is loaded through a render URL bound to the app, the published version (or the draft for a maker's preview), the interface, page, component and viewer. It expires after 10 minutes and is fetched again as needed; a newer published version replaces a stale frame.
- A block that navigates itself away is closed, with a Reload button.
The bridge: window.renly
The host injects a small, frozen bridge before anything else in the document. Every message between the block and the page carries a per-render nonce, and each side accepts messages only from the other's window.
| Call | What it does |
|---|---|
window.renly.run(operationId, params) | Runs one of the block's declared operations as the viewer and resolves with plain JSON. A query takes { cursor, search }; an aggregate takes nothing; an action takes { recordId, inputs, expectedVersion }. Errors reject with the server's plain message. |
window.renly.onTheme(cb) | Calls back with the current theme on ready and on every light or dark change: { mode, colors: { background, surface, text, muted, border, accent, positive, negative, warning }, series: [8 colours], font }. The same values are set as --renly-* CSS variables. |
window.renly.resize(height?) | Sets the frame's height, 80 to 2,000 pixels; with no value, fits the content. |
window.renly.operations | The declared operations, so a block can check what it has. |
<canvas id="chart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(async () => {
const by = await window.renly.run('open-by-status'); // a declared aggregate
new Chart(document.getElementById('chart'), {
type: 'bar',
data: { labels: by.groups.map(g => g.label), datasets: [{ data: by.groups.map(g => g.value) }] },
});
window.renly.onTheme(t => document.body.style.color = t.colors.text);
window.renly.resize();
})();
</script>
Declared operations
- A block declares operations in the designer: a query (rows), an aggregate (a number or a breakdown) or an action (one of the interface's declared actions). An operation id the block did not declare is refused, and a block cannot name a query or action that is not in the app.
- A query runs as a table of every field the viewer may read; an aggregate runs as a metric. Row scope, field access and source routing are exactly what those components get.
- An action runs through the page's action runner, so the person sees the action's inputs and the confirmation for deletes and archives, and the write rules apply. One action at a time per block. The audit entry records that it came from a custom block.
Limits
- 30 bridge calls a minute per viewer per block, data and actions together, counted across servers. Over the limit, calls fail with a Retry-After; the page applies the same limit before calling, and this limit never pauses the rest of the app.
- HTML up to 200,000 characters. Height from the component (default 320 pixels) or the block's own resize.
In the designer, and with the App Builder
The custom block editor has a monospace HTML editor, a height, and an operations editor (records, a number or breakdown, an action), with a live preview of the saved draft's block. A new block starts as a Chart.js bar chart fed by one aggregate. The App Builder knows the bridge and the no-network rule, so Build with AI can write a block for you.
An accepted trade-off, the same as for Sites: images from any https host are allowed and a block can navigate itself, so a block could carry data it was given to a host its maker chose. A block is written by a maker of the app, versioned with it, and only ever sees what the viewer may see.