Overview

Project the typed descriptor graph into UI — forms, tables, pickers, and reactive components, all bound to a descriptor with near-zero glue.

You already own a typed schema on both sides of the wire: the entity DSL (*.entity.ts) and the descriptor schemas (defineQuery / defineMutation carry effect/Schema for input, output, and target). Schema-driven UI projects that into the frontend — so a form, a table, or a picker is a binding to a descriptor the framework already has, not hand-written glue.

Here's that idea live — a form bound to a mutation and a table bound to a query, sharing one reactive backend. Add a row and it appears instantly, pushed from the server (it's your own throwaway sandbox; it resets on refresh):

<AutoForm api="app" mutation="todos.create" submitLabel="Add todo" />
<DataTable api="app" query="todos.list" />

Why this isn't "yet another admin generator"

Auto-generated CRUD UIs fail because they generate against a dead REST/SQL schema and hit a wall the moment you need anything custom. Voltro's version is different on one axis: the generated UI binds to a reactive, auto-optimistic backend. A <DataTable> is not a dead grid you refetch — it's a live subscription that updates on any write, with optimistic patches derived from the mutation descriptor's target, for free. The generation is the delivery; the reactivity is the value.

Binding symmetry — everything binds to a descriptor

UI piece binds to gives you
a form a mutation (todos.create) fields from its input Schema, validation, submit, op-correct optimistic
a field / picker a query (users.search) live options, debounce, pagination
a table a query (todos.list) live rows, sort, pagination, row-action mutations
a filter panel a query's input faceted controls + a live result count
a detail view a query + relations a live record + its eager-loaded relations

There's no new data layer to learn. Customization is only ever choosing how a binding renders — never re-plumbing the binding.

The customization ladder (never lose the binding)

This is where 99% of schema→UI generators die: they nail the happy path, then force a config DSL until it's worse than hand-written JSX — or you eject and lose everything. The one rule that avoids both:

Customization is a ladder of ever-smaller overrides, and on no rung do you lose the binding (schema validation + mutation submit + optimistic; for a picker, the live query). Eject from the rendering is never eject from the binding.

Rung How You lose
0 — schema annotation widget / label on the field nothing (zero form code)
1 — per-field render-prop <Field name="x">{f => <MyWidget .../>}</Field> nothing — state/validation/submit stay
2 — widget registry map a widget-kind → component app-wide nothing
3 — layout override arrange <Field/>s in your own JSX nothing
eject — headless useFormBinding / useDataTable + your JSX only the auto-rendering — the binding stays

<AutoForm> / <DataTable> are sugar over a headless core (useFormBinding / useDataTable) — which is exactly why ejecting from the rendering keeps everything underneath.

Localizing the built-in strings

The components render a handful of user-facing labels — Clear, Actions, Search…, the workflow status words (Running / Succeeded / …), presence's "X is editing" sentence, and so on — with English defaults. Localize them all at once by wrapping the app in UiStringsProvider, instead of threading a prop through every component:

import { UiStringsProvider } from '@voltro/web'

<UiStringsProvider
  strings={{
    filters: { clear: 'Zurücksetzen', resultCount: (n) => `${n} Ergebnisse` },
    dataTable: { actions: 'Aktionen' },
    workflow: { status: { running: 'Läuft' } },
    presence: { editingOne: (name, verb) => `${name} ist am ${verb}` },
  }}
>
  <App />
</UiStringsProvider>

Overrides deep-merge onto the defaults — supply only the sections and keys you change; everything else stays English. Interpolated strings (counts, names) are functions so a locale can reorder or pluralize. Providers nest (an inner one merges onto the outer), and a per-component prop (e.g. <AgentChat placeholder>) still wins over the context for a one-off tweak. useUiStrings() reads the active strings; defaultUiStrings is the English baseline.

Packaging

Three layers, all re-exported by @voltro/web so apps import everything from the framework's web entry:

  • Headless bindings (useFormBinding, useDataTable, useQueryField, …) → @voltro/client — pure logic, no kit dependency; works with any presentation (incl. an MUI/Mantine adapter).
  • The widget seam + components (<AutoForm> / <Field> / <DataTable> / <AsyncSelect> + the registry + accessible HTML defaults) → @voltro/ui.
  • The styled kit (Tailwind shadcnWidgets + tokens) → @voltro/ui-shadcn, which plugs into the seam via <WidgetRegistryProvider widgets={shadcnWidgets}>.

In this section

  • Forms & tables<AutoForm>, <DataTable>, query-bound pickers, the widget seam, skeletons, filters.
  • Reactive components — workflow progress, presence / multiplayer, AI chat — UI over the framework's durable + reactive backend.
  • Client utilitiesuseCan, useDerived, useProvenance, useUndo, usePreview, async validation, <RecordView>, the typed analytics catalog, the offline outbox, windowed subscriptions.