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.

users.setAge.mutation.ts
TypeScript
// 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.

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.