# Backfill Documentation > The accounting platform built for ambitious startups. An event-driven ledger, a powerful scripting runtime, and a dashboard that's actually yours. Pair it with whatever AI tool you already use. Site: https://backfill.io/ Last built: 2026-06-01 --- # File structure URL: https://backfill.io/docs/sdk/extensions/file-structure/ > The anatomy of a Backfill integration and how the platform finds what each file does. Every Backfill integration is a TypeScript project with a fixed layout. Hooks, jobs, API routes, custom entities, and fields are discovered by file path. UI pages are referenced from the manifest. ## Required files | File | Purpose | |---|---| | `backfill.config.ts` | Manifest. `defineExtension({...})` (or `defineConnector({...})`) as the default export. | | `package.json` | Declare `@backfill-io/sdk` as a devDependency plus any browser-compatible runtime packages your scripts import. | | `tsconfig.json` | TypeScript config — `strict: true`, `noEmit: true`. | | `.backfillrc` | Local dev credentials. Gitignored. | | `src/` | All script source. | ## What lives in `src/` ``` src/ ├── hooks// Hooks for a given entity │ ├── before-save.ts │ ├── after-save.ts │ └── recalculate.ts ├── jobs/ Scheduled jobs │ └── .ts ├── api/ HTTP routes │ └── .ts ├── entities/ Custom entity definitions │ └── .ts ├── fields/ Field declarations for host entities │ └── .ts ├── pages/ UI pages and action handlers │ └── .tsx └── lib/ Shared helpers └── .ts ``` Shared helpers usually live under `src/lib`. They are not registered on their own, but any hook, job, route, page, entity, tab, panel, or action can import them. The CLI bundles each executable entrypoint independently, so shared helper code is duplicated per entrypoint in v1. ## Where each thing goes | To do this… | Put a file at… | What it exports | |---|---|---| | React to an entity create / update | `src/hooks//after-save.ts` | `export default async function (ctx) { ... }` | | Modify or reject an entity write | `src/hooks//before-save.ts` | `export default async function (ctx) { return { ... } }` | | Contribute to draft calculation | `src/hooks//recalculate.ts` | See [Hooks → Recalculate](../hooks/#recalculate) | | Run on a schedule | `src/jobs/.ts` | `export const schedule = "..."`, `export async function run() {...}` | | Expose an HTTP endpoint | `src/api/.ts` | `export const GET = async (ctx) => {...}` (or `POST`, `PUT`, `PATCH`, `DELETE`) | | Define a custom entity | `src/entities/.ts` | `export default defineEntity({ name, fields })` | | Define fields on a host entity | `src/fields/.ts` | `export const stripeCustomerId = defineEntityField({ key: "stripe_customer_id", ... })` or `defineLineField(...)` | | Add a dashboard page | `src/pages/.tsx` + entry in `pages[]` | A default-export render function | | Add a tab / panel / action | A render `.tsx` + entry in `ui.{tabs,panels,actions}` | Same as a page render | --- # Get started URL: https://backfill.io/docs/sdk/get-started/ > Install the CLI, scaffold an extension, and ship your first hook. --- # Install the CLI URL: https://backfill.io/docs/sdk/get-started/install/ > Install @backfill-io/cli, point it at your workspace, ship. The `@backfill-io/cli` package handles scaffolding, dev mode, and deploys. One install and you're set. ## Install ```sh npm install -g @backfill-io/cli ``` Or with whatever package manager you prefer: ```sh pnpm add -g @backfill-io/cli yarn global add @backfill-io/cli ``` Verify: ```sh backfill --version ``` ## Point it at your workspace The CLI reads its server URL and API token from a `.backfillrc` file. It walks **up** the directory tree to find one — so a single file at the root of your integrations monorepo covers every project under it. ```ini # .backfillrc — DO NOT commit server = https://api.backfill.io token = bf_... ``` Precedence (highest wins): 1. CLI flags: `--server`, `--token` 2. Env vars: `BACKFILL_SERVER`, `BACKFILL_TOKEN` 3. A `.backfillrc` in the project directory 4. A `.backfillrc` in any parent directory Verify auth: ```sh backfill auth ``` If something's off, the CLI exits non-zero and tells you exactly what. ## What you can do with it | Command | What it does | |---|---| | `backfill init ` | Scaffold a new extension project. | | `backfill build` | Validate the manifest and every script. No deploy, no network. | | `backfill dev` | Watch your files. Auto-deploys a draft on save. | | `backfill deploy` | Build, validate, upload, activate. `--observe` to dry-run against live traffic. | | `backfill versions` | List deployed versions. | | `backfill rollback --to ` | Roll back to a prior version. | | `backfill logs [--follow]` | Stream extension logs. | | `backfill trigger` | Manually fire a hook for testing. | | `backfill promote` | Turn an `--observe` deployment into the live one. | | `backfill job list` / `run ` | Inspect or kick off a scheduled job by hand. | | `backfill auth` | Show or verify the active credentials. | ## Next Five-minute first ship → [Quickstart](../quickstart/). --- # Invoice URL: https://backfill.io/docs/resources/invoice/ --- # Overview URL: https://backfill.io/docs/sdk/connectors/overview/ > When to reach for defineConnector and what it gives you over a plain extension. **Connectors** vs **extensions**: - A **connector** (`defineConnector`) brings data in from an external provider — Stripe, Shopify, a bank, an OCR vendor. The platform handles streams, signed webhook ingestion, and canonical record emission. - An **[extension](../extensions/)** (`defineExtension`) augments Backfill's behavior — computing tax, validating fields, adding dashboard pages, posting to Slack on status changes. A connector is just an extension with extra connector-shaped manifest entries. Both share file layout, the standard library, and the deploy lifecycle. ## What changes ```typescript import { defineConnector } from "@backfill-io/sdk"; export default defineConnector({ key: "stripe", name: "Stripe", provider: "stripe", kind: "source", version: "0.2.0", permissions: { /* same shape as defineExtension */ }, connection: { testConnection: { route: "/setup/test_connection", method: "POST" }, setup: { webhookUrlTemplate: "...", instructions: "..." }, settingsSchema: { /* JSON Schema */ }, }, webhooks: { endpoint: "/webhooks/stripe", auth: "stripe_signature" }, streams: [ { key: "events", mode: "webhook" }, { key: "customers", mode: "poll", schedule: "*/15 * * * *" }, { key: "invoices", mode: "poll", schedule: "*/15 * * * *" }, /* ... */ ], }); ``` ## What you keep from `defineExtension` - File/folder structure under `src/` — hooks, jobs, api routes, entities all live in the same places. - The standard library globals: `Entity`, `Log`, `Http`, `Settings`, `Secrets`, `api`, and generated entity classes. - Permissions, custom entities, UI pages, UI contributions. ## What's added - **`connection`** — defines how a workspace admin sets the connector up (test connection route, setup instructions, settings schema). - **`webhooks`** — declares a single signed webhook ingress endpoint with provider-specific signature verification. - **`streams`** — the catalog of data streams this connector ingests (`poll` or `webhook` mode, with cron schedules for poll streams). - **`provider`, `kind`** — identity. `kind: "source"` means data flows in; the platform plans for `"sink"` (outbound) and bidirectional kinds in future. ## What's added at runtime Connector sync routes use a runtime global, **`ingest`**, that plain extensions don't get. It writes a normalized payload as a canonical entity, with idempotency keying and provenance: ```typescript import { ingest } from "@backfill-io/sdk"; ingest.canonical("customer", customerCanonical(stripeCustomer), { stream: "customers", idempotencyKey: `stripe:customer:${stripeCustomer.id}`, }); ``` Detail in [Sync routes](../sync-routes/). ## When `defineExtension` is enough If your integration only needs hooks and an admin settings UI, you don't need any of this. Plain extensions can call `Http`, write to entities, and run on schedules — they just don't get the connector-specific scaffolding for ingest, signed webhooks, or stream-level retries / checkpoints. --- # Pages URL: https://backfill.io/docs/sdk/extensions/ui/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 ```typescript 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: ```typescript 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`): ```tsx /** @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 (
); } ``` ## Available components Full prop reference for every primitive: [Components](../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: ```typescript interface ExtensionPageRenderContext { tenant: { id: string; slug?: string | null; name?: string | null }; settings: Record; 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: ```tsx 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. ```tsx import { Link, Routes } from "@backfill-io/sdk/ui"; Edit Open invoice ``` ## Form actions Forms post back to a separate handler script declared as a UI action — see [Contributions](../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. --- # Platform SDK URL: https://backfill.io/docs/sdk/ > Build on Backfill in TypeScript. Two flavors — extensions for behavior, connectors for data ingest. The SDK is split into two surfaces: - **[Extensions](extensions/)** — `defineExtension(...)`. Hooks, jobs, API routes, custom entities, dashboard pages, UI tabs, action buttons. The default flavor; reach for it for anything that augments Backfill's existing behavior. - **[Connectors](connectors/)** — `defineConnector(...)`. Third-party data integrations with first-class support for streams, signed webhooks, and the canonical ingest pipeline. Reach for it when your job is *bringing data in*. Both share the same file/folder layout, the same standard-library globals, and the same deploy lifecycle. A connector is just an extension with extra connector-shaped manifest entries. If you're new, start with [Get started](get-started/install/) and the [Quickstart](get-started/quickstart/), then read [Concepts](concepts/) end-to-end. --- # Standard library URL: https://backfill.io/docs/sdk/concepts/standard-library/ > The named exports from @backfill-io/sdk that every script imports. Every script and declaration file uses the same small standard library, exported from `@backfill-io/sdk`. | Export | What it does | Reference | |---|---|---| | `Entity` | Generic CRUD against any entity. | [Entities](../../extensions/entities/) | | `Log` | Structured logging — `debug`, `info`, `warn`, `error`. | [Logging](../../extensions/logging/) | | `Http` | Outbound HTTPS. URLs gated by `permissions.http`. | [Outbound HTTP](../../extensions/outbound-http/) | | `Settings` | Read merged extension settings. | [Settings](../../extensions/settings/) | | `Secrets` | Read/write encrypted secrets. | [Secrets](../../extensions/secrets/) | | `api` | Helper for declaring API route handlers and building responses. | [API routes](../../extensions/api-routes/) | | `OAuth` | Connector-only access to host-managed provider OAuth tokens and metadata. | [Connector OAuth](../../connectors/oauth/) | ```typescript import { Entity, Log, Settings } from "@backfill-io/sdk"; ``` The runtime also exposes these as globals — at deploy, `@backfill-io/sdk` imports are stripped and the same identifiers are injected onto `globalThis`. Either style runs identically; the docs use imports because they give you autocomplete, type-checking, and a clear edit-time signal of what each script depends on. `@backfill-io/sdk` also exports declaration helpers — `defineExtension`, `defineConnector`, `defineEntity`, `defineEntityField`, `defineLineField` — used in the manifest and discovered declaration files. They return the declaration object for type-checking and build-time discovery; they are not runtime APIs. Each is documented alongside the file it belongs in: [configuration](../../extensions/configuration/), [connectors](../../connectors/overview/), [custom entities](../../extensions/custom-entities/), and [custom fields](../../extensions/custom-fields/). ## Entity classes The SDK exports a typed class for each first-class entity, pre-bound to the type: ``` Invoice Customer Payment Expense SalesReceipt Item VendorBill BillPayment PurchaseOrder CreditMemo Quote SalesOrder RefundReceipt VendorCredit Account Vendor JournalEntry Deposit BankTransfer ``` ```typescript import { Invoice } from "@backfill-io/sdk"; ``` So instead of `Entity.query("Invoice", { ... })` you write `Invoice.query({ ... })`. Both work and read against the same data. Custom entities you declare via `defineEntity` get the same treatment — once the CLI regenerates types, they're importable by their PascalCase name. --- # Build a tax extension URL: https://backfill.io/docs/sdk/guides/build-a-tax-extension/ > A full extension that adds a sales-tax line to invoices and exposes a tax-summary endpoint and dashboard page. A complete extension that exercises hooks, custom entities, jobs, API routes, and pages. ## Goal - On invoice / sales-receipt save, compute and stamp a sales-tax line. - Track each tax application as a `TaxRecord` custom entity. - Track jurisdiction addresses as a `NexusAddress` custom entity, manageable from a UI page. - Expose a `/tax-summary` API route for date-ranged reporting. - Run a weekly job that summarizes the prior week. ## Manifest ```typescript // backfill.config.ts import { defineExtension } from "@backfill-io/sdk"; export default defineExtension({ key: "backfill-extension-tax-calcs", name: "Tax Calculations", version: "1.1.0", permissions: { entities: { Invoice: ["read", "write"], SalesReceipt: ["read", "write"], NexusAddress: ["read", "write"], }, }, 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: "page", }, ], ui: { actions: [ { id: "save-nexus-address", label: "Save Nexus Address", page: "nexus-address-form", run: "src/pages/actions/save-nexus-address.ts", }, ], }, config: { taxRate: 8.25, taxLabel: "Sales Tax", enableTaxOnInvoices: true, enableTaxOnSalesReceipts: true, }, }); ``` ## Custom entities ```typescript // src/entities/NexusAddress.ts import { defineEntity } from "@backfill-io/sdk"; export default defineEntity({ name: "NexusAddress", fields: { nexusName: { type: "string", required: true }, street: { type: "string", required: true }, city: { type: "string", required: true }, state: { type: "string", required: true }, postalCode: { type: "string", required: true }, country: { type: "string", default: "US" }, active: { type: "boolean", default: true }, }, }); ``` ```typescript // src/entities/TaxRecord.ts import { defineEntity } from "@backfill-io/sdk"; export default defineEntity({ name: "TaxRecord", fields: { entityType: { type: "string", required: true }, entityId: { type: "string", required: true }, subtotal: { type: "number", required: true }, taxRate: { type: "number", required: true }, taxAmount: { type: "number", required: true }, }, }); ``` ## The before-save hook ```typescript // src/hooks/invoice/before-save.ts import { Settings, Log } from "@backfill-io/sdk"; import type { HookContext } from "@backfill-io/sdk"; export default async function (ctx: HookContext) { const settings = Settings.getAll(); if (!settings.enableTaxOnInvoices) return null; const rate = settings.taxRate ?? 8.25; const label = settings.taxLabel ?? "Sales Tax"; const lines: any[] = ctx.entity.line_items ?? []; const subtotal = lines .filter((li) => li.description !== label) .reduce((sum, li) => sum + (Number(li.amount) || 0), 0); if (subtotal <= 0) return null; const taxAmount = Math.round(subtotal * (rate / 100) * 100) / 100; const updatedLines = [ ...lines.filter((li) => li.description !== label), { description: label, amount: taxAmount, quantity: 1 }, ]; const totalAmount = Math.round((subtotal + taxAmount) * 100) / 100; Log.info("Tax calculated for invoice", { subtotal, rate, taxAmount, totalAmount }); return { line_items: updatedLines, total_amount: totalAmount }; } ``` A near-identical hook lives at `src/hooks/sales-receipt/before-save.ts`. ## The after-save hook After save, persist a `TaxRecord` so we can summarize later: ```typescript // src/hooks/invoice/after-save.ts declare const TaxRecord: any; import type { HookContext } from "@backfill-io/sdk"; export default async function (ctx: HookContext) { const lines: any[] = ctx.entity.line_items ?? []; const taxLine = lines.find((li) => li.description?.toLowerCase().includes("tax")); if (!taxLine) return; const subtotal = lines .filter((li) => li !== taxLine) .reduce((sum, li) => sum + (Number(li.amount) || 0), 0); TaxRecord.create({ entityType: "Invoice", entityId: ctx.entity.id, subtotal, taxRate: 8.25, taxAmount: Number(taxLine.amount), }); } ``` ## The summary API route ```typescript // src/api/tax-summary.ts declare const TaxRecord: any; export const GET = async function (ctx: { searchParams: Record }) { const { start, end } = ctx.searchParams; if (!start || !end) { return { __api_response: true, status: 400, body: { error: "Missing start/end" } }; } const records = TaxRecord.query({ where: { _created_at: { gte: start, lte: end } }, }); return { period: { start, end }, count: records.length }; }; ``` ## The weekly job ```typescript // src/jobs/weekly-tax-report.ts import { Log } from "@backfill-io/sdk"; declare const TaxRecord: any; export const schedule = "0 8 * * 1"; export async function run() { const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const startDate = weekAgo.toISOString().split("T")[0]; const records: any[] = TaxRecord.query({ where: { _created_at: { gte: startDate } }, }); const total = records.reduce((sum, r) => sum + r.taxAmount, 0); Log.info("Weekly tax report", { count: records.length, total }); } ``` ## Nexus address pages Three pieces wire up the admin UI: a list page, a form page, and an action handler that does the save. ### List page ```tsx // src/pages/nexus-addresses.tsx import { Page, Section, Stack, Card, Text, Badge, EmptyState, Toolbar, Link, Routes, } from "@backfill-io/sdk/ui"; import type { ExtensionPageRenderContext } from "@backfill-io/sdk/ui"; declare const NexusAddress: any; export default function render(ctx: ExtensionPageRenderContext) { const rows = NexusAddress.query({ orderBy: [{ field: "nexusName", direction: "asc" }], }); const newHref = Routes.page(ctx, "nexus-address-form"); return ( Add Nexus Address} > {rows.length === 0 ? (
) : (
{rows.map((row: any) => ( {row.street} {`${row.city}, ${row.state} ${row.postalCode}`} {row.country || "US"} {row.active === false && Inactive} Edit ))}
)}
); } ``` `Routes.page(ctx, "", { query })` builds a link to another page in this extension, with optional query params. That's how the list page hands a row id to the form page on edit. ### Form page The same `nexus-address-form.tsx` file handles both create and edit — the difference is whether `?id=...` is in the URL. If it is, we load the existing record and pre-fill the fields. ```tsx // src/pages/nexus-address-form.tsx import { Page, Section, Stack, Grid, Form, Field, TextInput, Select, Checkbox, SubmitButton, Routes, } from "@backfill-io/sdk/ui"; import type { ExtensionPageRenderContext, SelectOption } from "@backfill-io/sdk/ui"; declare const NexusAddress: any; const COUNTRY_OPTIONS: SelectOption[] = [ { value: "US", label: "United States" }, { value: "CA", label: "Canada" }, { value: "GB", label: "United Kingdom" }, // …extend as needed ]; export default function render(ctx: ExtensionPageRenderContext) { const id = typeof ctx.page.params?.id === "string" ? ctx.page.params.id : undefined; const existing = id ? NexusAddress.get(id) : null; const isEdit = existing != null; const v = existing ?? { nexusName: "", street: "", city: "", state: "", postalCode: "", country: "US", active: true, }; return (
``` ### `` Flex container. Use for stacking children with consistent gaps. | Prop | Type | Default | |---|---|---| | `direction` | `"row"` \| `"column"` | `"column"` | | `gap` | `"xs"` \| `"sm"` \| `"md"` \| `"lg"` | `"md"` | | `span` | `1` - `12` | Optional grid column span on medium screens and up. | | `align` | `"start"` \| `"center"` \| `"end"` \| `"stretch"` | Optional cross-axis alignment. | | `justify` | `"start"` \| `"center"` \| `"end"` \| `"between"` | Optional main-axis distribution. | | `self` | `"start"` \| `"center"` \| `"end"` \| `"stretch"` | Optional alignment of this stack inside a parent grid. | | `wrap` | `boolean` | Allow row children to wrap. | | `children` | node | — | ```tsx Live Synced 2 minutes ago ``` Use `justify="end"` and `self="center"` for row action groups in dense grids: ```tsx ``` ### `` Fixed-column responsive grid. | Prop | Type | Default | |---|---|---| | `columns` | `1` - `12` | `1` | | `gap` | `"xs"` \| `"sm"` \| `"md"` \| `"lg"` | `"md"` | | `children` | node | — | ```tsx ``` ### `` Bordered container with optional title. | Prop | Type | Notes | |---|---|---| | `title` | `string` | Card header. | | `href` | `string` | Optional same-origin destination; makes the card container clickable. | | `tone` | `"default"` \| `"subtle"` | Optional background/border treatment. | | `density` | `"default"` \| `"compact"` | Optional tighter padding. | | `children` | node | Card body. | ```tsx ``` Use `href` when a repeated card should open a detail page. Nested buttons still invoke their own actions. ```tsx {week.customerName} ``` Use `tone="subtle"` for lightweight form grouping without adding custom CSS: ```tsx ... ``` ### `` Container for action buttons, typically passed to ``. | Prop | Type | |---|---| | `children` | node | ```tsx New vendor} > ... ``` ### `` Render multiple children without a wrapping element. Returned implicitly when you write `<>...`. ```tsx First line Second line ``` ## Display ### `` Plain text with optional tone. | Prop | Type | Default | |---|---|---| | `tone` | `"default"` \| `"muted"` | `"default"` | | `children` | node | — | ```tsx No matches found. ``` ### `` Big-number display. Both `label` and `value` accept strings, numbers, or booleans. | Prop | Type | Notes | |---|---|---| | `label` | string \| number \| boolean | Required. | | `value` | string \| number \| boolean | Required. | | `tone` | `"default"` \| `"good"` \| `"bad"` \| `"warning"` \| `"neutral"` | Optional value color. | | `children` | node | Optional caption beneath the value. | ```tsx across 12 invoices ``` ### `` Section separator with an optional centered label. | Prop | Type | Notes | |---|---|---| | `label` | `string` | Optional. | ```tsx ``` ### `` Small colored chip. | Prop | Type | Default | |---|---|---| | `tone` | `"info"` \| `"success"` \| `"warning"` \| `"error"` \| `"neutral"` | `"neutral"` | | `children` | node | — | ```tsx Needs review ``` ### `` Placeholder for empty result sets. | Prop | Type | Notes | |---|---|---| | `title` | `string` | Required. | | `description` | `string` | Optional supporting copy. | ```tsx {rows.length === 0 ? :
} ``` ### `
` Tabular data. Pass an array of plain-object rows and a column descriptor. | Prop | Type | Notes | |---|---|---| | `columns` | `TableColumn[]` | Required. | | `rows` | `Record[]` | Required. | ```typescript type TableColumn = { key: string; // matches a key on each row label: string; align?: "start" | "center" | "end"; cell?: | { kind: "badge"; tone?: BadgeTone } | { kind: "link"; hrefKey: string; labelKey?: string } | { kind: "action"; action: string; label?: string; labelKey?: string; params?: Record; paramsKey?: string } | { kind: "money"; currency?: string; currencyKey?: string } | { kind: "date" }; }; ``` ```tsx import { Customer } from "@backfill-io/sdk";
``` ## Interaction ### ` ``` For navigation, use [``](#link). `