One schema.
From Postgres to React props.
The promise "end-to-end TypeScript" gets made by every modern framework, then quietly broken at the database boundary. Voltro keeps the promise: tables, mutations, queries, errors, AI tool inputs, workflow payloads — all defined through effect/Schema, all flowing through to your client without a codegen step you can forget to run.
// 1. Define the table — types live in the schema.
export const users = table('users', {
id: id(), // UserId (branded)
email: text().unique(), // string
age: integer().nullable(), // number | null
})
// 2. Define the mutation — input/output schemas.
export const setAge = defineMutation({
name: 'users.setAge',
input: Schema.Struct({ id: UserId, age: Schema.Number }),
output: Schema.Struct({ id: UserId, age: Schema.Number }),
error: AgeOutOfRange,
})
// 3. On the client — fully typed, zero codegen.
const setAgeMutation = useMutation('app', 'users.setAge')
// ^? MutationBuilder<{ id: UserId; age: number }, ...>
try {
const { id, age } = await setAgeMutation.mutate({ id, age: 12 })
} catch (e) {
if (e instanceof AgeOutOfRange) {
// e.range is typed: { min: number; max: number }
}
}Six guarantees the compiler enforces for you.
Effect Schema everywhere
One schema library — effect/Schema — defines tables, mutations, queries, workflow payloads, RPC errors, AI tool inputs. No tRPC, no Zod-vs-Yup-vs-Joi split, no schema bridges.
Branded TypeIDs
id() columns return branded types (UserId, OrgId) — never raw strings. Passing an OrgId to a function that expects a UserId fails compilation. Foreign keys flow with the brand.
No codegen step
The RPC group writes itself on every voltro dev boot. Your client just imports `useMutation('app', 'users.setAge')` — the schema type flows through. No `pnpm codegen`. No CI failure when someone forgets to run it.
Typed errors via pattern-matching
Declare error: AgeOutOfRange on a mutation. Throw the class on the server. instanceof on the client narrows to the typed fields. Same for tagged errors via Effect.catchTag.
Relation types flow through .with()
database.users.with(...) — declaring nested relations like profile and posts.author — returns User[] with a fully-typed nested shape: Profile | null on each, Author on each post. Arbitrary depth.
Auto-optimistic patches are typed
The target: declaration on a mutation drives the cache patch reducer with the same type as the subscription's row type. You can't prepend a row of the wrong shape; it won't compile.
The codegen step you forget is the bug your customer finds.
The classic tRPC / GraphQL-codegen workflow has one weak link: someone forgets to run `pnpm codegen` before push. The client compiles against the old schema. CI is green. Production is broken. Voltro's codegen runs on every voltro dev boot AND on every file change — there's no out-of-band step you can miss.
Three layers where Voltro's types prevent bugs your tests wouldn't catch:
- • Branded IDs. A handler that takes orgId: string accepts whatever a caller passes. A handler that takes orgId: OrgId catches the bug at the call-site, before the test even runs.
- • Typed errors. A client that catch (e: unknown) and writes if (e.message === '...') drifts on every error-message edit. Pattern-matching on a tagged class never does.
- • Relation eager-loading. A query that returns user.profile.firstName but the eager spec only fetches user.profile.id fails at runtime — undefined.firstName. Voltro's .with() spec types the result so the access is checked.
Types flow through every other primitive.
Schema DSL
Table descriptors are the source of truth. Branded IDs, JSON-typed columns, oneOf() literal narrowing — all typed through.
Reactive queries
useSubscription returns the same shape the server's query declares. Eager-loaded relations are typed.
Durable workflows
Workflow payload + success types flow through the wire when triggered from a mutation. No retype-cast at the boundary.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.