Overview

How Voltro's reactive data layer works — queries, mutations, actions, streams, all over one WebSocket with typed errors and tracked dependencies.

The api side of a Voltro app is a collection of typed RPC primitives. You drop descriptor/server-executor pairs into the api tree; the framework discovers them; the typed client lights up.

This section covers how data flows between server and client.

A form bound to a mutation, a table bound to a query, one reactive backend — add a row and it appears instantly, pushed from the server (your own throwaway sandbox; it resets on refresh):

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

The shape of it

┌──────────────────────────────────────────────────────────────────┐
│  Client (React)                                                  │
│    useSubscription('app', 'notes.list', input)                   │
│    useMutation('app', 'notes.create')                            │
│    useAction('app', 'invites.send')                              │
│    useAgentStream('app', 'support.run')                          │
└──────────────────────────┬───────────────────────────────────────┘
                           │  @effect/rpc over WebSocket

┌────────────────────────────────────────────────────────────────────┐
│  api app                                                           │
│    *.query.ts    + *.query.server.ts     — reactive reads          │
│    *.mutation.ts + *.mutation.server.ts  — transactional writes    │
│    *.action.ts   + *.action.server.ts    — external I/O / unary    │
│    *.stream.ts   + *.stream.server.ts    — one-shot element push   │
│    *.route.tsx                            — public raw-HTTP route  │
│    *.workflow.tsx  + *.trigger.tsx        — durable work + events  │
│    *.agent.tsx                            — persisted AI chat      │
└────────────────────────────────────────────────────────────────────┘

Most of it goes through one WebSocket (REST routes are the public raw-HTTP exception). Queries and streams are both streaming RPCs, but they mean different things: a query emits reactive snapshot/delta envelopes; a stream emits plain elements and then finishes.

Every primitive at a glance

Voltro ships more building blocks than this one section holds — each is a typed descriptor in a file the CLI discovers. The five at the top live here; the rest have their own sections but are the same kind of thing. The complete set:

Primitive File What it is
Query *.query.ts reactive read → Queries
Mutation *.mutation.ts transactional write → Mutations
Action *.action.ts unary external I/O → Actions
Stream *.stream.ts one-shot element push → Streams
REST route *.route.tsx public raw-HTTP endpoint → REST routes
Aggregate *.aggregate.ts scheduled materialised query → Aggregates
Subscriber *.subscribe.ts per-table post-commit reaction → Subscribers
Workflow *.workflow.tsx durable multi-step work → Workflows
Event trigger *.trigger.tsx event → workflow fan-out → Event triggers
Schedule *.cron.tsx cron-fired handler → Scheduling
Agent *.agent.tsx persisted AI chat → Agents
Tool *.tool.tsx model-callable function → Tools
Seed *.seed.ts boot/data seeding → Seeds
Startup *.startup.tsx run-once boot hook (long-lived work + teardown) → Startup hooks
Email *.email.tsx React-Email template → Mail
Webhook *.webhook.tsx signed inbound / outbound → Webhooks

What's in this section

PrimitivesQueries (reactive reads + dependency tracking) · Mutations (transactional writes, typed errors, auto-optimistic) · Actions (unary RPC, no transaction) · Subscriptions (the reactive engine behind snapshots/deltas) · Streams (defineStream element push) · REST routes (public raw-HTTP for third parties) · Aggregates (scheduled materialised queries) · Subscribers (per-table post-commit reactions).

Protocol & errorsWire protocol (framing, multiplexing) · Error handling (Schema-tagged errors, retries, client narrowing).

The contract

Every query, mutation, action, and stream has two files:

queries/notes.list.query.ts          # descriptor: browser-safe schema + metadata
queries/notes.list.query.server.ts   # executor: server-only implementation

The descriptor defines the wire surface:

defineX({
  name:   'feature.action',
  input:  Schema.Struct({ /* ... */ }),
  output: Schema.Struct({ /* mutations/actions */ }),
  element: Schema.Struct({ /* streams */ }),
  error:  Schema.Union(ErrorVariantA, ErrorVariantB),
})

The server executor receives (input, ctx) and can return a plain value, Promise, Effect, query descriptor, or Stream, depending on the primitive. Everything is Schema-validated at the boundary. The TypeScript types flow to the client automatically through rpcGroup.generated.ts.

name charset. A procedure name is .-separated and becomes a JS identifier in rpcGroup.generated.ts (todos.listtodosListRpc). It must start with a letter and use only letters, digits, and . / - / _. - and _ are camelCased away, so two names differing only by separator collide. Codegen rejects an invalid or colliding name at voltro dev boot, naming the file. Prefer plain camelCase segments (todos.list).

Reactivity model

Queries are live. A query descriptor declares source: 'tableName'; the server executor returns either a query builder/descriptor or a computed value. When a mutation writes to the table, the runtime recomputes affected subscriptions and pushes a snapshot or delta.

Streams are not live subscriptions. A stream executor returns a one-shot Stream of elements. Use streams for transient token feeds, progress events, logs, or external event sources. If the result should survive reloads or update other tabs, persist rows and expose them through a query.

Tenant scoping

Every executor receives ctx.request.subject — the typed identity of the caller. Tables with the tenant() mixin auto-scope reads and writes. See Multi-tenancy for the full story.

When to use what

You want… Use
Read data that updates live *.query.ts + *.query.server.ts + useSubscription
Write data atomically *.mutation.ts + *.mutation.server.ts + useMutation
External I/O / unary side effect *.action.ts + *.action.server.ts + useAction
Public HTTP endpoint for a third party (URL + JSON) *.route.tsx (defineRestRoute)
Transient server-to-client element stream *.stream.ts + *.stream.server.ts + useAgentStream
Durable persisted AI chat *.agent.tsx or action + query over agent_messages
Background job *.workflow.tsx
Fan a domain event out to one or more workflows *.trigger.tsx + ctx.events.emit(...)
Pre-computed query result (top-N, summary) *.aggregate.ts
React to every commit on a table (server-side) *.subscribe.ts
Event ingestion + analytical aggregates over events Analytics sink

Pages don't have to pick only one. Most use useSubscription for reads and useMutation for writes; add actions or streams only when the workflow calls for them.