Dein Schema hat das Backend bereits typisiert.
Jetzt baut es die UI.

Jede Voltro-Procedure trägt ein effect/Schema — Input, Output und die Tabelle, in die sie schreibt. Schema-driven UI projiziert das ins Frontend: Ein Formular ist ein Binding an eine Mutation, eine Tabelle ein Binding an eine Query. Du bekommst Felder, Spalten, Validierung und live auto-optimistische Updates, ohne den Glue-Code zu schreiben. Und weil das Binding ein reaktives, auto-optimistisches Backend ist — kein totes REST-Schema — ist die generierte Tabelle eine Live-Subscription, kein Grid, das du neu lädst.

todos.tsx
TypeScript
// The form IS the mutation. Fields come from its input Schema;
// validation, submit, and op-correct optimistic are wired for you.
import { AutoForm, DataTable } from '@voltro/web'

// create-vs-edit is just two mutations — no "CRUD mode" switch
<AutoForm api="app" mutation="todos.create" />
<AutoForm api="app" mutation="todos.update" defaults={row} />

// The table IS the query. Columns from its output Schema; rows are a
// LIVE subscription — they update on any write, with no refetch.
<DataTable
  api="app"
  query="todos.list"
  rowActions={(r) => <button onClick={() => del.mutate({ id: r.id })}>Delete</button>}
/>

Der Descriptor ist die einzige Quelle der Wahrheit.

Du hast das input-Schema, das output-Schema und die Ziel-Tabelle bereits einmal deklariert, auf dem Server. Schema-driven UI liest diese — es verlangt nie, dass du deine Daten in einer Formular-Config neu beschreibst. Annotation passt an, wie ein Feld rendert; sie verdrahtet nie neu, woran das Feld gebunden ist.

Formulare binden an eine Mutation

AutoForm liest das input-Schema der Mutation und rendert die Felder: eine Literal-Union → ein Select, ein String → Text, ein Boolean → eine Checkbox. Validierung, Submit und op-korrektes Auto-Optimistic gibt es gratis. Client und Server teilen exakt dasselbe Schema.

Tabellen binden an eine Query

DataTable liest das output-Schema der Query für die Spalten und subscribed für die Zeilen. Die Liste aktualisiert sich bei jedem Write — kein Refetch, kein Cache zu invalidieren. pageSize aktiviert ein Live-"Load more", das das Fenster wachsen lässt und weiter streamt.

Picker binden an eine Query

Eine reference()-Spalte wird standardmäßig zu einem debounced, live Typeahead auf der konventionellen Such-Query. AsyncSelect (oder das headless useQueryField) gibt dir Create-on-the-fly-Comboboxen ohne Konfiguration — die Optionsliste ist selbst reaktiv.

Eine Anpassungs-Leiter, keine Klippe

Feld annotieren → ein Widget per Render-Prop → eine Widget-Art app-weit tauschen → das ganze Layout selbst besitzen → auf den headless Hook ejecten. Auf KEINER Sprosse verlierst du das Binding. Genau hier stirbt jeder andere Schema→UI-Generator; hier ist es das Design.

Reaktive Komponenten, nicht nur CRUD

Drop-in-UI für das durable + reaktive Backend: WorkflowProgress für eine Live-Schritt-Timeline, PresenceAvatars / EditingIndicator für Multiplayer, AgentChat für einen reconnect-sicheren AI-Chat. Jede hat einen headless Hook darunter.

Ein Werkzeugkasten gebundener Hooks

useCan (gaten auf denselben Scopes, die der Server durchsetzt), usePreview (eine Mutation in einer zurückgerollten Transaktion als Dry-Run ausführen), useUndo (universelles Ctrl-Z über Mutationen), useAsyncValidation, RecordView, ein typisierter Analytics-Katalog, eine Offline-Outbox, gefensterte Subscriptions. Alles Projektionen desselben Descriptor-Graphen.

Zucker obendrauf, headless darunter.

headless.tsx
TypeScript
// Every component is sugar over a headless binding. Eject from the
// RENDERING without losing the binding (schema + submit + optimistic).
import { useFormBinding, useDataTable } from '@voltro/client'

const form  = useFormBinding('app', 'todos.create')
// → { fields, values, errors, isValid, pending, setValue, submit, reset }

const table = useDataTable('app', 'todos.list', { pageSize: 25 })
// → { columns, rows, loading, sort, toggleSort, loadMore, hasMore }
// rows stay reactive as the window grows. Render any way you like.

Warum die generierte Variante die handgeschriebene tatsächlich schlägt.

Auto-generiertes CRUD scheitert meist, weil es gegen ein totes REST/SQL-Schema generiert und gegen eine Wand läuft, sobald du irgendetwas Eigenes brauchst. Voltros Variante ist auf einer Achse anders: Die generierte UI bindet an eine reaktive, auto-optimistische Runtime. Die Generierung ist die Auslieferung; die Reaktivität ist der Wert.

Eine Änderung, zwei Oberflächen:

Füge der Tabelle eine Spalte hinzu → das Entity-Schema ändert sich → das input-Schema der Mutation ändert sich → AutoForm lässt das Feld wachsen UND DataTable lässt die Spalte wachsen, beide typisiert, beide validiert, beim nächsten Reload. Keine Formular-Library zu aktualisieren, keine Spalten-Config zu synchronisieren, kein optimistischer Reducer von Hand. Ejecte auf den headless Hook an dem Tag, an dem du den Defaults entwächst — das Binding (Schema + Submit + Live-Updates) überlebt den Eject.

Es komponiert mit dem Rest der Runtime.

Öffne das Framework. Schau es dir selbst an.

Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.