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.
// 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.
// 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.
Reaktive Queries
Die Live-Subscription hinter jeder DataTable. Ein Hook, push-basiert, auto-optimistisch — die Datenschicht, an die die UI bindet.
End-to-End-Typen
Das effect/Schema, das das Formular typisiert, ist dasselbe, das die Wire und den Handler typisiert. Ändere es einmal; der Compiler findet jede Lücke.
AI-Primitive
AgentChat + AppAgent binden an einen synthetisierten Agent; exposeAsTool macht aus jeder Procedure ein sicheres LLM-Tool, begrenzt durch die Permissions des Aufrufers.
Ö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.