Blueprint, in your codebase.
Two libraries: the runtime that renders a contract into real UI on the client, and the SDK that fills a contract with your data on the server — shipping per language, Node, Go and Java. No eval, no lock-in; you host both.
@dashforge/blueprint · runtime@dashforge/blueprint-nodeblueprint-goblueprint-javaThe library
@dashforge/blueprint is an open UI contract plus a thin runtime that renders it. JSON goes in, a real screen comes out. The runtime walks each node and maps it to one of your own components — there is no interpreter, no template language, and no eval: the contract describes the screen, the behavior stays in your typed React. That's what keeps a config-driven UI auditable and CSP-clean — the runtime runs no code from the JSON, and one malformed node degrades to an inline fallback instead of taking down the whole tree.
Add it to your app
One package — it bundles both the Tailwind and MUI flavor bindings. Peers: React 19+ and zod 4.
npm i @dashforge/blueprint
Using the MUI flavor? add its runtime too: @mui/material, @emotion/react and @emotion/styled.
Render a contract
Spread a contract, choose a flavor, and hand the runtime whatever the screen needs. Everything is a prop — the JSON stays pure data.
import { DashBlueprint } from '@dashforge/blueprint' export function Screen({ contract, user }) { return ( <DashBlueprint {...contract} lib="tw" forms={{ signin: { onSubmit: (values) => auth(values) } }} resolveData={(source) => catalog[source]} /> ) }
What you pass:
{...contract}— the{ version, root }object you loaded (from a CDN, your backend, or a file).rootis the node tree.lib—"tw"or"mui", the flavor its atoms render through. Both ship in the package.forms/slots/customNodes/icons/intl— the escape hatches: where your real, typed React plugs in (submit handlers, custom components, icons, i18n).resolveData— the client-side counterpart of the server SDK: it fills any$data.*list the contract left dynamic. Resolve it on the server with the SDK, or here on the client — your call.
Server or client. <DashBlueprint> is SSR-safe — it touches no browser-only APIs at render, so you can server-render it (Next.js, Remix, renderToString) for the first paint, then hydrate. It's interactive, so in the App Router it lives inside a client boundary ('use client'). One caveat: supply the same intl.t on server and client, or the two renders diverge and hydration mismatches.
What the JSON holds
A contract is a declarative description of the screen — and nothing else: a version and a root node tree. Each node carries its type (an atom), its props, children, and optional visibility, access and i18n. It never carries logic: no onClick, no eval, no code-from-strings.
{ "version": "1.0", "root": { "type": "form", "nodeId": "signin", "children": [ { "type": "field", "props": { "name": "email", "label": "Email", "type": "email" } }, { "type": "select", "props": { "name": "role", "options": { "source": "$data.roles" } }, "visibility": { "field": "$form.mode", "eq": "team" } }, { "type": "submit", "props": { "label": "Sign in" } } ] } }
Drop in real React
Anything the closed atom catalog doesn't cover, you inject as real, typed React — the wall always has a door. Three hatches, all props on DashBlueprint:
<DashBlueprint {...contract} lib="tw" customNodes={{ CurrencyField }} currency={<CurrencyField name="currency" />} slots={{ 'legal-copy': <LegalCopy /> }} forms={{ signin: { onSubmit } }} />
customNodes— for a component the catalog doesn't have. The contract carries a{ type: "custom", props: { component: "CurrencyField", … } }node, and your component is mounted with thoseprops. Use this when you control the contract, or the Builder emits a custom node.slots— override an existing node by itsnodeId; your element renders in its place, as-is. So if the Builder authored an input whosenodeIdiscurrency, mounting your own field iscurrency={<CurrencyField name="currency" />}— the camelCase shorthand forslots={{ currency: … }}(use theslotsobject form for kebab-case ids likelegal-copy).forms— bind a form node'sonSubmit(alsoonValuesChange,defaultValues) by itsnodeId.
The key is the node's nodeId — its addressing key, not the form name, though they often match. And a slot override renders your element verbatim: for a form field it does not inherit the field's binding, so wire your component to the form yourself — or author the field as a custom node, which receives the node's props.
Tailwind or MUI
One contract, either design system. Set lib="tw" and every atom renders through @dashforge/tw; set lib="mui" and the same contract renders through @mui/material. Both flavor packs ship inside @dashforge/blueprint — no separate install, no registration step. There is no Blueprint theme baked into the output: atoms adopt your tokens and components, so a Blueprint screen looks like the rest of your app, not like Blueprint. Write the contract once; switch the flavor with one prop.
The full surface
Every prop on <DashBlueprint>. Only version and root are required — everything else is opt-in.
| Prop | What it does |
|---|---|
version | The contract version — "1.0". Required. |
root | The root node of the contract tree. Required. |
lib | "tw" or "mui" — the flavor its atoms render through. |
customNodes | Map a custom node's component id to your React component (it receives the node's props). |
forms | Per-nodeId form config: onSubmit, plus defaultValues, schema/resolver, mode, onError. |
slots | Override a node by its nodeId with your element (wins over the atom binding). |
<camelCaseId> | Top-level shorthand for slots — any camelCase prop maps to that nodeId. |
icons | Icon registry — maps the icon ids the contract references to render functions (lucide, MUI, custom SVG). |
intl | i18n config (t, locale?, registry?) — resolves $t refs. Absent → keys render literally. |
resolveData | Resolve a $data.* list binding on the client — the counterpart of the server SDK. |
rules | Named visibility predicates a contract can reference by name. |
validationMode | "strict" / "lenient". Env-aware default: strict in prod, lenient in dev. |
onValidationDiagnostics | Fires after every compile with { errors, warnings } — ship it to your logger. |
metadata | Arbitrary passthrough carried alongside the contract. |