Pages
Custom dashboard pages declared in backfill.config.ts and rendered with the SDK's JSX runtime.
UI pages are the only script type not discovered by filename — the platform needs metadata (id, title, route, kind, nav order) it can’t read off the filesystem, so pages are declared in the manifest with a render path pointing at the .tsx source.
Manifest declaration
interface ExtensionPageConfig {
id: string;
title: string;
path: string;
render: string;
kind?: 'page' | 'settings' | 'list' | 'dashboard' | 'detail';
parentPage?: string;
access?: { roles?: string[] };
adminOnly?: boolean;
nav?: { label: string; section?: string; order?: number };
}
A real example:
pages: [
{
id: "nexus-addresses",
title: "Nexus Addresses",
path: "nexus-addresses",
render: "src/pages/nexus-addresses.tsx",
kind: "list",
nav: { label: "Nexus Addresses", section: "Tax", order: 10 },
},
{
id: "nexus-address-form",
title: "Nexus Address",
path: "nexus-address-form",
render: "src/pages/nexus-address-form.tsx",
kind: "detail",
parentPage: "nexus-addresses",
},
],
Field reference
| Field | Effect |
|---|---|
id | Unique within the extension. Used to reference the page from ui.actions[].page. |
title | Browser title and page header. |
path | URL segment under the extension’s namespace. Cannot contain ... |
render | Path to the .tsx render script. Discovery resolves it against compiled files. |
kind | "page", "settings", "list", "dashboard", or "detail". Affects layout and where the page surfaces. |
parentPage | Required for kind: "detail". References another page id in this extension, and Backfill renders the default back link. |
access.roles | Restrict to users in specific roles. |
adminOnly | Convenience: equivalent to access: { roles: ["admin"] }. Cannot combine with access. |
nav.label | Sidebar label (defaults to no nav entry if omitted). |
nav.section | Sidebar section grouping. Defaults to "Extensions". |
nav.order | Integer for sorting within the section. |
All fields are validated at deploy.
Render script
Render scripts use the SDK’s JSX runtime (@backfill-io/sdk/jsx-runtime) and component library (@backfill-io/sdk/ui):
/** @jsxImportSource @backfill-io/sdk */
import { Page, Section, Card, Stat, Table } from "@backfill-io/sdk/ui";
declare const NexusAddress: any;
export default function NexusAddresses() {
const rows = NexusAddress.query({ where: { active: true } });
return (
<Page title="Nexus Addresses">
<Section>
<Card>
<Stat label="Active addresses" value={rows.length} />
</Card>
</Section>
<Table
rows={rows}
columns={[
{ key: "nexusName", label: "Name" },
{ key: "state", label: "State" },
{ key: "city", label: "City" },
]}
/>
</Page>
);
}
Available components
Full prop reference for every primitive: Components.
Quick list — exported from @backfill-io/sdk/ui:
Fragment, Page, Section, Stack, Grid, Card, Stat, Divider, Text, Badge, EmptyState, Toolbar, Button, Link, Table, Form, Field, HiddenInput, NumberInput, DateInput, DateTimeInput, TextInput, Textarea, Select, Checkbox, Toggle, SubmitButton, ResetButton.
Each has a typed prop set. Passing an unsupported prop or a value of the wrong type throws at runtime.
Render context
The platform invokes your render with a context object:
interface ExtensionPageRenderContext {
tenant: { id: string; slug?: string | null; name?: string | null };
settings: Record<string, any>;
page: {
id: string;
title: string;
path: string;
kind?: string | null;
parentPageId?: string | null;
params?: RouteParams;
};
routes?: ExtensionPageRoutesContext;
actor?: { id?: string; email?: string; role?: string };
extension?: { id?: string; versionId?: string; key?: string };
}
If your render takes no params, ignore it; otherwise:
export default function NexusAddressForm(ctx: ExtensionPageRenderContext) {
const id = ctx.page.params?.id;
const existing = id ? NexusAddress.get(String(id)) : null;
// ...
}
Route helpers
Use Routes.page(ctx, pageId, { query }) to link between pages in the same
extension. Use Routes.entity(ctx, entityName, id) to link to the canonical
Backfill detail page for a standard entity.
import { Link, Routes } from "@backfill-io/sdk/ui";
<Link href={Routes.page(ctx, "nexus-address-form", { query: { id: row.id } })}>
Edit
</Link>
<Link href={Routes.entity(ctx, "Invoice", invoiceId)}>
Open invoice
</Link>
Form actions
Forms post back to a separate handler script declared as a UI action — see Contributions.
What the render runs in
UI render scripts are not React components. The runtime walks the returned BackfillElementNode tree, validates props server-side, and produces the dashboard’s UI. There’s no useState, no client-side reactivity in the script — re-render happens by re-running the render with new params.